@payez/next-mvp 4.1.11 → 4.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/dist/api-handlers/account/profile.d.ts +38 -0
  2. package/dist/api-handlers/account/profile.js +73 -45
  3. package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
  4. package/dist/api-handlers/account/recovery/send-code.js +52 -18
  5. package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
  6. package/dist/api-handlers/account/recovery/verify-code.js +52 -18
  7. package/dist/api-handlers/account/verify-email.js +15 -1
  8. package/dist/api-handlers/account/verify-sms.js +15 -1
  9. package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
  10. package/dist/api-handlers/auth/passkey-signin.js +97 -0
  11. package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
  12. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  13. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  14. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  15. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  16. package/dist/api-handlers/mfa/methods.js +41 -0
  17. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  18. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  19. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  20. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  21. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  22. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  23. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  24. package/dist/api-handlers/passkeys/list.js +29 -0
  25. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  26. package/dist/api-handlers/passkeys/manage.js +54 -0
  27. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  28. package/dist/api-handlers/passkeys/register-options.js +41 -0
  29. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  30. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  31. package/dist/api-handlers/session/viability.js +19 -0
  32. package/dist/auth/better-auth.d.ts +68 -5
  33. package/dist/auth/better-auth.js +256 -42
  34. package/dist/auth/utils/idp-client.d.ts +13 -22
  35. package/dist/auth/utils/idp-client.js +12 -82
  36. package/dist/client/better-auth-client.d.ts +14 -2
  37. package/dist/lib/session-store.js +28 -21
  38. package/dist/lib/startup-init.js +19 -19
  39. package/dist/lib/token-lifecycle.js +40 -4
  40. package/dist/models/SessionModel.d.ts +17 -0
  41. package/dist/pages/login/page.d.ts +10 -1
  42. package/dist/pages/login/page.js +69 -80
  43. package/dist/pages/verify-code/page.d.ts +10 -1
  44. package/dist/pages/verify-code/page.js +72 -4
  45. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  46. package/dist/routes/auth/passkey-signin.js +18 -0
  47. package/dist/routes/auth/viability.d.ts +2 -0
  48. package/dist/routes/auth/viability.js +88 -0
  49. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  50. package/dist/routes/mfa/backup-code-verify.js +5 -0
  51. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  52. package/dist/routes/mfa/backup-codes.js +14 -0
  53. package/dist/routes/mfa/methods.d.ts +21 -0
  54. package/dist/routes/mfa/methods.js +10 -0
  55. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  56. package/dist/routes/mfa/totp-confirm.js +14 -0
  57. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  58. package/dist/routes/mfa/totp-enroll.js +14 -0
  59. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  60. package/dist/routes/mfa/totp-verify.js +18 -0
  61. package/dist/routes/passkeys/list.d.ts +10 -0
  62. package/dist/routes/passkeys/list.js +14 -0
  63. package/dist/routes/passkeys/manage.d.ts +18 -0
  64. package/dist/routes/passkeys/manage.js +13 -0
  65. package/dist/routes/passkeys/register-options.d.ts +10 -0
  66. package/dist/routes/passkeys/register-options.js +14 -0
  67. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  68. package/dist/routes/passkeys/register-verify.js +14 -0
  69. package/dist/server/auth.d.ts +2 -2
  70. package/dist/server/decode-session.js +37 -3
  71. package/package.json +958 -893
  72. package/scripts/check-internal-url-usage.sh +73 -73
  73. package/src/api/index.ts +17 -17
  74. package/src/api-handlers/account/masked-info.ts +45 -45
  75. package/src/api-handlers/account/profile.ts +99 -80
  76. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  77. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  78. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  79. package/src/api-handlers/account/reset-password.ts +23 -23
  80. package/src/api-handlers/account/send-code.ts +76 -76
  81. package/src/api-handlers/account/validate-password.ts +118 -118
  82. package/src/api-handlers/account/verify-email.ts +15 -1
  83. package/src/api-handlers/account/verify-sms.ts +15 -1
  84. package/src/api-handlers/admin/stats.ts +249 -249
  85. package/src/api-handlers/anon/preferences.ts +123 -123
  86. package/src/api-handlers/auth/jwks.ts +20 -20
  87. package/src/api-handlers/auth/login.ts +240 -240
  88. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  89. package/src/api-handlers/auth/validate.ts +43 -43
  90. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  91. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  92. package/src/api-handlers/mfa/methods.ts +46 -0
  93. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  94. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  95. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  96. package/src/api-handlers/passkeys/list.ts +31 -0
  97. package/src/api-handlers/passkeys/manage.ts +54 -0
  98. package/src/api-handlers/passkeys/register-options.ts +42 -0
  99. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  100. package/src/api-handlers/session/viability.ts +161 -142
  101. package/src/auth/better-auth.ts +247 -6
  102. package/src/auth/unauthenticated-routes.ts +2 -2
  103. package/src/auth/utils/idp-client.ts +13 -95
  104. package/src/client/better-auth-client.ts +70 -70
  105. package/src/client/fetchWithSession.ts +21 -21
  106. package/src/client/index.ts +13 -13
  107. package/src/client/useAnonSession.ts +131 -131
  108. package/src/components/admin/VibeAdminContext.tsx +87 -87
  109. package/src/config/logging-config.ts +82 -82
  110. package/src/config/unauthenticated-routes.ts +19 -19
  111. package/src/edge/internal-api-url.ts +65 -65
  112. package/src/edge/middleware.ts +42 -42
  113. package/src/hooks/useProfile.ts +75 -75
  114. package/src/lib/anon-session.ts +213 -213
  115. package/src/lib/auth-secret.ts +137 -137
  116. package/src/lib/demo-mode.ts +17 -17
  117. package/src/lib/idp-client-config.ts +559 -559
  118. package/src/lib/idp-fetch.ts +126 -126
  119. package/src/lib/internal-api.ts +171 -171
  120. package/src/lib/jwt-decode-client.ts +45 -45
  121. package/src/lib/rate-limit-service.ts +9 -9
  122. package/src/lib/redis.ts +26 -26
  123. package/src/lib/refresh-token-validator.ts +64 -64
  124. package/src/lib/secret-validation.ts +8 -8
  125. package/src/lib/session-store.ts +702 -695
  126. package/src/lib/session.ts +34 -34
  127. package/src/lib/startup-init.ts +251 -251
  128. package/src/lib/token-expiry.ts +40 -40
  129. package/src/lib/token-lifecycle.ts +533 -497
  130. package/src/lib/types/api-responses.ts +336 -336
  131. package/src/middleware/auth-decision.ts +43 -43
  132. package/src/models/DecodedAccessToken.ts +16 -16
  133. package/src/models/SessionModel.ts +282 -264
  134. package/src/pages/admin-roles/index.ts +9 -9
  135. package/src/pages/login/page.tsx +123 -228
  136. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  137. package/src/pages/verify-code/page.tsx +71 -5
  138. package/src/routes/account/index.ts +31 -31
  139. package/src/routes/account/masked-info.ts +36 -36
  140. package/src/routes/account/send-code.ts +39 -39
  141. package/src/routes/account/verify-email.ts +40 -40
  142. package/src/routes/account/verify-sms.ts +40 -40
  143. package/src/routes/auth/index.ts +22 -22
  144. package/src/routes/auth/passkey-signin.ts +13 -0
  145. package/src/routes/auth/refresh.ts +48 -48
  146. package/src/routes/auth/session.ts +166 -166
  147. package/src/routes/auth/viability.ts +89 -0
  148. package/src/routes/index.ts +17 -17
  149. package/src/routes/mfa/backup-code-verify.ts +1 -0
  150. package/src/routes/mfa/backup-codes.ts +10 -0
  151. package/src/routes/mfa/methods.ts +22 -0
  152. package/src/routes/mfa/totp-confirm.ts +10 -0
  153. package/src/routes/mfa/totp-enroll.ts +10 -0
  154. package/src/routes/mfa/totp-verify.ts +14 -0
  155. package/src/routes/passkeys/list.ts +10 -0
  156. package/src/routes/passkeys/manage.ts +23 -0
  157. package/src/routes/passkeys/register-options.ts +10 -0
  158. package/src/routes/passkeys/register-verify.ts +10 -0
  159. package/src/routes/session/index.ts +7 -7
  160. package/src/routes/session/refresh-viability.ts +17 -17
  161. package/src/server/auth.ts +373 -373
  162. package/src/server/decode-session.ts +235 -202
  163. package/src/services/signalrActivityService.ts +258 -258
  164. package/src/theme/ThemeProvider.tsx +39 -39
  165. package/src/theme/default.ts +33 -33
  166. package/src/theme/index.ts +31 -31
  167. package/src/theme/types.ts +69 -69
  168. package/src/theme/useTheme.ts +57 -57
  169. package/src/theme/utils.ts +40 -40
  170. package/src/types/api.ts +12 -12
  171. package/src/types/auth.d.ts +23 -23
  172. package/src/types/auth.ts +22 -22
  173. package/src/types/logging.ts +11 -11
  174. package/src/types/recovery.ts +54 -54
  175. package/src/utils/api.ts +353 -353
  176. package/src/utils/circuitBreaker.ts +40 -40
  177. package/src/utils/error-message.ts +108 -108
  178. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -213,25 +213,25 @@ async function performInitialization() {
213
213
  const connectionLine = isConnectionError
214
214
  ? '║ 🔌 CONNECTION REFUSED - IDP appears to be down ║\n║ ║\n'
215
215
  : '';
216
- console.error(`
217
- ╔══════════════════════════════════════════════════════════════╗
218
- ║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
219
- ║ ║
220
- ║ The app cannot start without a valid auth signing secret. ║
221
- ║ This should be fetched from IDP at startup. ║
222
- ║ ║
223
- ${connectionLine}║ Possible causes: ║
224
- ║ • IDP is not running or unreachable ║
225
- ║ • CLIENT_ID is not registered in IDP ║
226
- ║ • IDP_URL is incorrect ║
227
- ║ • Network connectivity issue ║
228
- ║ ║
229
- ║ Current config: ║
230
- ║ • IDP_URL: ${idpUrl}║
231
- ║ • CLIENT_ID: ${clientId}║
232
- ╚══════════════════════════════════════════════════════════════╝
233
-
234
- [STARTUP] Error: ${errorMsg}
216
+ console.error(`
217
+ ╔══════════════════════════════════════════════════════════════╗
218
+ ║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
219
+ ║ ║
220
+ ║ The app cannot start without a valid auth signing secret. ║
221
+ ║ This should be fetched from IDP at startup. ║
222
+ ║ ║
223
+ ${connectionLine}║ Possible causes: ║
224
+ ║ • IDP is not running or unreachable ║
225
+ ║ • CLIENT_ID is not registered in IDP ║
226
+ ║ • IDP_URL is incorrect ║
227
+ ║ • Network connectivity issue ║
228
+ ║ ║
229
+ ║ Current config: ║
230
+ ║ • IDP_URL: ${idpUrl}║
231
+ ║ • CLIENT_ID: ${clientId}║
232
+ ╚══════════════════════════════════════════════════════════════╝
233
+
234
+ [STARTUP] Error: ${errorMsg}
235
235
  `);
236
236
  // Re-throw so callers know initialization failed
237
237
  throw lastInitError;
@@ -250,8 +250,35 @@ async function ensureFreshToken(request) {
250
250
  idpRefreshToken: idpTokens?.idpRefreshToken,
251
251
  idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
252
252
  || (baSession.session?.expiresAt ? new Date(baSession.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
253
- mfaVerified: true,
254
- oauthProvider: 'google',
253
+ /**
254
+ * ⚠ THESE WERE HARDCODED `true` / `'google'`. (QAPert finding #3,
255
+ * BAPert ruling, card 167892.)
256
+ *
257
+ * This rebuild path assumed EVERY session is a Google OAuth sign-in
258
+ * that completed MFA. It reads `idpTokens` for everything else —
259
+ * including roles, two lines up — and then IGNORED the stored
260
+ * mfaVerified sitting in that same object and asserted `true`.
261
+ *
262
+ * WHY IT MATTERS MORE THAN A WRONG FIELD: a passkey sign-in stores
263
+ * an HONEST mfaVerified derived from the token's `amr`. On any
264
+ * Redis miss this path OVERWROTE that false with true — so the
265
+ * session claimed an MFA the user never performed, and the honest
266
+ * derivation upstream was not weakened but VOIDED. Same defect
267
+ * class as the mints that assert completion they never did,
268
+ * arriving through a cache-miss rather than a mint.
269
+ *
270
+ * `oauthProvider: 'google'` was the second lie on the same line:
271
+ * a passkey session rebuilt here was relabelled a Google sign-in,
272
+ * destroying the provenance the sign-in path deliberately records.
273
+ * The IDP emits NO provider claim (DotNetPert), so the honest
274
+ * answer when it was not stored is UNSET — not a guess.
275
+ *
276
+ * Both now default to the SAFE direction: absent/false is
277
+ * fail-closed at every consumer we measured (`!!mfaVerified`,
278
+ * `mfaVerified || ... || false`).
279
+ */
280
+ mfaVerified: idpTokens?.mfaVerified ?? false,
281
+ oauthProvider: idpTokens?.oauthProvider ?? baSession.oauthProvider ?? undefined,
255
282
  idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId ?? baSession.idpClientId,
256
283
  merchantId: idpTokens?.merchantId ?? baSession.merchantId,
257
284
  };
@@ -267,8 +294,17 @@ async function ensureFreshToken(request) {
267
294
  name: betterAuthSession.user.name,
268
295
  roles: [],
269
296
  idpAccessTokenExpires: Date.now() + 24 * 60 * 60 * 1000,
270
- mfaVerified: true,
271
- oauthProvider: 'google',
297
+ /**
298
+ * ⚠ ALSO HARDCODED `true` / `'google'`, and this path is the worse of
299
+ * the two: it runs with NO IDP TOKENS AT ALL — the comment above says
300
+ * so — and still asserted that MFA was completed. It fabricated an
301
+ * assurance claim from nothing, for every consumer, not just passkey.
302
+ *
303
+ * With nothing to derive from, FALSE is the only honest value and
304
+ * UNSET is the only honest provider. Both are fail-closed.
305
+ */
306
+ mfaVerified: false,
307
+ oauthProvider: undefined,
272
308
  };
273
309
  }
274
310
  if (!sessionData) {
@@ -48,6 +48,23 @@ export interface SessionData {
48
48
  bearerKeyId?: string;
49
49
  /** Whether MFA has been verified for this session */
50
50
  mfaVerified: boolean;
51
+ /**
52
+ * The IDP's own routing decision for a 2FA-pending login (cards 141688 /
53
+ * 141741, IDP WO 113023). Wire name is snake — `next_step`.
54
+ *
55
+ * 'enroll_mfa' — no USABLE method (confirmed and not revoked). The user
56
+ * cannot complete a challenge; they must enrol first.
57
+ * 'mfa_verify' — a usable method exists; challenge them.
58
+ * null — fully authenticated, nothing outstanding.
59
+ *
60
+ * RELAYED, NEVER DERIVED. The IDP owns this fact and is already usable-aware;
61
+ * recomputing it here from a method count would make a second owner and would
62
+ * get the revoked-method case wrong (count>0 but usable=0).
63
+ *
64
+ * NOT A COMPLETION SIGNAL. 'mfa_verify' means "can be challenged", not
65
+ * "is through". `mfaVerified` remains the only field that says they verified.
66
+ */
67
+ nextStep?: 'enroll_mfa' | 'mfa_verify' | null;
51
68
  /** The MFA method used (email, sms, totp) */
52
69
  mfaMethod?: 'email' | 'sms' | 'totp';
53
70
  /** When MFA was completed (Unix timestamp ms) */
@@ -19,4 +19,13 @@
19
19
  * 2. Wrap your app with ThemeProvider to customize branding
20
20
  * 3. Optionally add your health check component
21
21
  */
22
- export default function LoginPage(): import("react/jsx-runtime").JSX.Element;
22
+ import React from 'react';
23
+ /**
24
+ * @param altAuthSlot an additional sign-in control (e.g. a passkey link)
25
+ * rendered INSIDE the card, after the social row. Optional: omitting it
26
+ * leaves the page byte-identical to before, so existing consumers are
27
+ * unaffected.
28
+ */
29
+ export default function LoginPage({ altAuthSlot }?: {
30
+ altAuthSlot?: React.ReactNode;
31
+ }): import("react/jsx-runtime").JSX.Element;
@@ -21,9 +21,6 @@
21
21
  * 3. Optionally add your health check component
22
22
  */
23
23
  'use client';
24
- var __importDefault = (this && this.__importDefault) || function (mod) {
25
- return (mod && mod.__esModule) ? mod : { "default": mod };
26
- };
27
24
  Object.defineProperty(exports, "__esModule", { value: true });
28
25
  exports.default = LoginPage;
29
26
  const jsx_runtime_1 = require("react/jsx-runtime");
@@ -31,19 +28,66 @@ const react_1 = require("react");
31
28
  const better_auth_client_1 = require("../../client/better-auth-client");
32
29
  const navigation_1 = require("next/navigation");
33
30
  const react_2 = require("react");
34
- const ReservedStatusBox_1 = __importDefault(require("../../components/reserved/ReservedStatusBox"));
35
- const ReservedRecoveryWarning_1 = __importDefault(require("../../components/reserved/ReservedRecoveryWarning"));
36
31
  const useTheme_1 = require("../../theme/useTheme");
37
- function LoginForm() {
32
+ function LoginForm({ altAuthSlot }) {
38
33
  const searchParams = (0, navigation_1.useSearchParams)();
39
34
  const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
40
35
  const urlError = searchParams?.get('error');
36
+ // Better Auth mount redirects OAuth failures here as ?oauth_error={provider}
37
+ // (the failure-signal guard from the merge spec). Distinct from `error`.
38
+ const oauthError = searchParams?.get('oauth_error');
41
39
  const { data: sessionData, isPending } = better_auth_client_1.authClient.useSession();
42
40
  const session = sessionData;
43
41
  const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
44
42
  const branding = (0, useTheme_1.useBranding)();
45
43
  const colors = (0, useTheme_1.useColors)();
46
44
  const layout = (0, useTheme_1.useLayout)();
45
+ // Social sign-in. The button renders ONLY when the IDP client config has an
46
+ // enabled provider row — discovered from the public settings route (the same
47
+ // config the Better Auth engine builds providers from), never assumed.
48
+ // Absent row / fetch failure = no button. Fail quiet is correct here.
49
+ const [googleEnabled, setGoogleEnabled] = (0, react_1.useState)(false);
50
+ const [googleLoading, setGoogleLoading] = (0, react_1.useState)(false);
51
+ (0, react_1.useEffect)(() => {
52
+ let mounted = true;
53
+ fetch('/api/auth/settings', { cache: 'no-store' })
54
+ .then((r) => (r.ok ? r.json() : null))
55
+ .then((json) => {
56
+ if (!mounted || !json)
57
+ return;
58
+ const providers = json?.data?.enabledProviders;
59
+ if (Array.isArray(providers) && providers.some((p) => typeof p === 'string' && p.toLowerCase() === 'google')) {
60
+ setGoogleEnabled(true);
61
+ }
62
+ })
63
+ .catch(() => undefined); // fail quiet: no button
64
+ return () => { mounted = false; };
65
+ }, []);
66
+ const handleGoogleSignIn = async () => {
67
+ setGoogleLoading(true);
68
+ setLoginError(null);
69
+ try {
70
+ // Pre-clean any stale session so account-switch works (idealvibe
71
+ // account-auth/login idiom: signOut before signIn.social).
72
+ await better_auth_client_1.authClient.signOut().catch(() => undefined);
73
+ // The package's OWN SDK, not a raw fetch — Jon's ruling (QA 17400 /
74
+ // BAPert 17401): the page is the template consumers copy, so it teaches
75
+ // the same one-line idiom idealvibe uses (49cc95f, four call sites).
76
+ // The SDK hits POST /api/auth/sign-in/social itself, with its own
77
+ // credentials mode, baseURL and error contract, and navigates on the
78
+ // returned authorize URL.
79
+ await better_auth_client_1.authClient.signIn.social({
80
+ provider: 'google',
81
+ // The social round trip lands on '/', not the form's /dashboard
82
+ // default: the post-callback session upgrade decides the destination.
83
+ callbackURL: searchParams?.get('callbackUrl') || '/',
84
+ });
85
+ }
86
+ catch {
87
+ setGoogleLoading(false);
88
+ setLoginError('Google sign-in is unavailable right now. Please try again.');
89
+ }
90
+ };
47
91
  // Helper to create lighter/darker shades for hover states
48
92
  const adjustBrightness = (hex, percent) => {
49
93
  const num = parseInt(hex.replace('#', ''), 16);
@@ -71,6 +115,13 @@ function LoginForm() {
71
115
  const [showRecoveryOptions, setShowRecoveryOptions] = (0, react_1.useState)(false);
72
116
  // Force show form after timeout if stuck in loading
73
117
  const [forceShowForm, setForceShowForm] = (0, react_1.useState)(false);
118
+ // Surface OAuth failures the Better Auth mount bounced back here.
119
+ (0, react_1.useEffect)(() => {
120
+ if (oauthError) {
121
+ const name = oauthError.charAt(0).toUpperCase() + oauthError.slice(1);
122
+ setLoginError(`${name} sign-in did not complete. Please try again.`);
123
+ }
124
+ }, [oauthError]);
74
125
  // Handle URL errors with user-friendly messages
75
126
  (0, react_1.useEffect)(() => {
76
127
  if (urlError) {
@@ -115,63 +166,9 @@ function LoginForm() {
115
166
  }, 1000);
116
167
  return () => clearTimeout(immediateTimeout);
117
168
  }, [status, session]);
118
- const handleSubmit = async (e) => {
119
- e.preventDefault();
120
- // Atomic state: set to submitting (clears error and success)
121
- setIsSubmitting(true);
122
- setLoading(true);
123
- setLoginError(null);
124
- setLoginSuccess(false);
125
- try {
126
- console.log('[LOGIN] Starting authentication...');
127
- const result = await better_auth_client_1.authClient.signIn.email({
128
- email,
129
- password,
130
- callbackURL: callbackUrl,
131
- });
132
- if (result?.error) {
133
- console.log('[LOGIN] Authentication failed:', result.error);
134
- setIsSubmitting(false);
135
- setLoading(false);
136
- setLoginSuccess(false);
137
- const errorMsg = typeof result.error === 'object'
138
- ? result.error.message || 'Authentication failed'
139
- : String(result.error);
140
- if (errorMsg.includes('password') || errorMsg.includes('Password')) {
141
- console.log('[LOGIN] Password error detected - showing recovery options');
142
- setShowRecoveryOptions(true);
143
- setLoginError(errorMsg);
144
- }
145
- else if (errorMsg.includes('Unable to connect')) {
146
- setLoginError('The authentication service is currently unavailable. Please try again later.');
147
- }
148
- else {
149
- setLoginError(errorMsg);
150
- }
151
- return;
152
- }
153
- // Atomic state: set to success
154
- console.log('[LOGIN] Authentication successful!');
155
- setLoginSuccess(true);
156
- setLoginError(null);
157
- setIsSubmitting(false);
158
- // Get updated session
159
- const freshSession = await better_auth_client_1.authClient.getSession();
160
- console.log('[LOGIN] Fresh session obtained, redirecting to 2FA...');
161
- // Redirect to verify-code for 2FA
162
- const verifyUrl = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
163
- console.log('[LOGIN] Redirecting to:', verifyUrl);
164
- window.location.href = verifyUrl;
165
- }
166
- catch (err) {
167
- console.error('[LOGIN] Unexpected error:', err);
168
- // Atomic state: set to error
169
- setIsSubmitting(false);
170
- setLoading(false);
171
- setLoginSuccess(false);
172
- setLoginError(err instanceof Error ? err.message : 'An error occurred during login');
173
- }
174
- };
169
+ // handleSubmit REMOVED with the email/password form it served: it called
170
+ // authClient.signIn.email, which better-auth always rejected because this
171
+ // package never configures emailAndPassword. See the note at the render site.
175
172
  // Show loading spinner for initial loads
176
173
  if (!forceShowForm && !loginError && (loading || (status === 'loading' && !session))) {
177
174
  return ((0, jsx_runtime_1.jsxs)("div", { className: `flex flex-col items-center justify-center min-h-screen ${colors.background}`, children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-6 w-6", style: { color: buttonColor }, viewBox: "0 0 24 24", fill: "none", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] }), (0, jsx_runtime_1.jsx)("p", { className: `mt-4 text-sm ${colors.muted}`, children: status === 'loading' ? 'Loading...' : 'Authenticating...' })] }));
@@ -204,27 +201,19 @@ function LoginForm() {
204
201
  .themed-status-submitting svg {
205
202
  color: ${buttonColor} !important;
206
203
  }
207
- ` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full", children: [(0, jsx_runtime_1.jsxs)("div", { className: `rounded-2xl shadow-xl p-8 ${colors.card} ${colors.border} text-gray-800`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-8", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-3xl font-bold mb-2", style: { color: 'inherit' }, children: branding.tagline || 'Welcome Back' }), (0, jsx_runtime_1.jsx)("p", { className: colors.muted, children: "Sign in to your account" })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-6", "data-testid": "login-form", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "email", className: "block text-sm font-medium mb-2", style: { color: 'inherit' }, children: "Email Address" }), (0, jsx_runtime_1.jsx)("input", { id: "email", type: "email", required: true, autoFocus: true, autoComplete: "email", value: email, onChange: (e) => setEmail(e.target.value), className: `themed-input w-full px-4 py-3 border border-gray-300 rounded-lg transition-colors`, placeholder: "you@example.com", disabled: isSubmitting, style: { backgroundColor: 'white', color: '#1f2937' } })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "password", className: "block text-sm font-medium mb-2", style: { color: 'inherit' }, children: "Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { id: "password", type: showPassword ? "text" : "password", required: true, autoComplete: "current-password", value: password, onChange: (e) => setPassword(e.target.value), className: `themed-input w-full px-4 py-3 pr-12 border border-gray-300 rounded-lg transition-colors`, placeholder: "\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022", disabled: isSubmitting, style: { backgroundColor: 'white', color: '#1f2937' } }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "absolute right-3 top-1/2 transform -translate-y-1/2 hover:opacity-70", style: { color: 'inherit', opacity: 0.7 }, "aria-label": showPassword ? "Hide password" : "Show password", children: showPassword ? ((0, jsx_runtime_1.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.464 6.464m7.535 7.535l3.415 3.414M3 3l3.464 3.464M21 21l-3.415-3.414" }) })) : ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: [(0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }), (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })] })) })] })] }), (0, jsx_runtime_1.jsx)(ReservedStatusBox_1.default, { candidates: [
208
- "Ready",
209
- "Authenticating...",
210
- "The authentication service is currently unavailable. Please try again later.",
211
- "Invalid email or password. Please try again.",
212
- "Login successful! Redirecting..."
213
- ], containerClass: "p-3 text-sm leading-relaxed rounded-lg border", iconSizePx: 16, children: isSubmitting ? (
214
- /* Themed: Submitting */
215
- (0, jsx_runtime_1.jsxs)("div", { className: "themed-status-submitting flex items-start space-x-2 p-3 rounded-lg", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin w-4 h-4 mt-0.5 flex-shrink-0", fill: "none", viewBox: "0 0 24 24", style: { color: buttonColor }, children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("span", { className: "themed-status-submitting-text text-sm leading-relaxed", style: { color: buttonColor }, children: "Authenticating..." })] })) : loginError ? (
216
- /* Red: Error */
217
- (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start space-x-2 p-3 rounded-lg bg-red-500/10 border border-red-500/20", children: [(0, jsx_runtime_1.jsx)("div", { className: "w-4 h-4 bg-red-500 rounded-full mt-0.5 flex-shrink-0" }), (0, jsx_runtime_1.jsx)("span", { className: "text-red-600 text-sm leading-relaxed", children: loginError })] })) : loginSuccess ? (
218
- /* Green: Success */
219
- (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start space-x-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20", children: [(0, jsx_runtime_1.jsx)("div", { className: "w-4 h-4 bg-green-500 rounded-full mt-0.5 flex-shrink-0" }), (0, jsx_runtime_1.jsx)("span", { className: "text-green-600 text-sm leading-relaxed", children: "Login successful! Redirecting..." })] })) : (
220
- /* Ready State / Health Check Placeholder */
221
- (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start space-x-2 p-3 rounded-lg bg-green-500/10 border border-green-500/20", children: [(0, jsx_runtime_1.jsx)("div", { className: "w-4 h-4 bg-green-500 rounded-full mt-0.5 flex-shrink-0" }), (0, jsx_runtime_1.jsx)("span", { className: "text-green-600 text-sm leading-relaxed", children: "Ready" })] })) }), (0, jsx_runtime_1.jsx)(ReservedRecoveryWarning_1.default, { show: showRecoveryOptions, titleText: "Account Lockout Warning", bodyText: "Your account will be locked after one more failed attempt. Need help?", actionLabel: "Start Account Recovery", containerClass: "p-4 bg-amber-50 border border-amber-200 rounded-lg", titleClass: "text-amber-900 font-medium mb-2", bodyClass: "text-amber-800 text-sm mb-3", buttonClass: "w-full bg-amber-600 hover:bg-amber-700 text-white font-medium py-2 px-4 rounded-md text-sm transition-colors", iconSizePx: 20, children: showRecoveryOptions && ((0, jsx_runtime_1.jsx)("div", { className: "p-4 bg-amber-50 border border-amber-200 rounded-lg", children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-start space-x-3", children: [(0, jsx_runtime_1.jsx)("svg", { className: "w-5 h-5 text-amber-600 mt-0.5 flex-shrink-0", fill: "currentColor", viewBox: "0 0 20 20", children: (0, jsx_runtime_1.jsx)("path", { fillRule: "evenodd", d: "M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z", clipRule: "evenodd" }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex-1", children: [(0, jsx_runtime_1.jsx)("h3", { className: "text-amber-900 font-medium mb-2", children: "Account Lockout Warning" }), (0, jsx_runtime_1.jsx)("p", { className: "text-amber-800 text-sm mb-3", children: "Your account will be locked after one more failed attempt. Need help?" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => window.location.href = `/account-auth/recovery?email=${encodeURIComponent(email)}`, className: "w-full bg-amber-600 hover:bg-amber-700 text-white font-medium py-2 px-4 rounded-md text-sm transition-colors", children: "Start Account Recovery" })] })] }) })) }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: isSubmitting, className: "themed-button w-full text-white font-semibold py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center", children: isSubmitting ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin -ml-1 mr-3 h-5 w-5 text-white", xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Signing in..."] })) : ('Sign In') })] }), (0, jsx_runtime_1.jsx)("div", { className: `mt-6 text-center text-sm ${colors.muted}`, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "themed-link hover:underline", children: "Start account recovery" }) })] }), (0, jsx_runtime_1.jsxs)("p", { className: `mt-4 text-center text-sm ${colors.muted}`, children: ["Don't have an account?", ' ', (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/register", className: "themed-link font-medium hover:underline", children: "Sign up" })] })] }) })] }));
204
+ ` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-md w-full", children: [(0, jsx_runtime_1.jsxs)("div", { className: `rounded-2xl shadow-xl p-8 ${colors.card} ${colors.border} text-gray-800`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "text-center mb-8", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-3xl font-bold mb-2", style: { color: 'inherit' }, children: branding.tagline || 'Welcome Back' }), (0, jsx_runtime_1.jsx)("p", { className: colors.muted, children: "Sign in to your account" })] }), googleEnabled && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => void handleGoogleSignIn(), disabled: googleLoading || isSubmitting, "data-testid": "google-sign-in", className: "w-full px-4 py-3 mb-6 rounded-lg border border-gray-300 font-semibold transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-50", style: { backgroundColor: 'white', color: '#1f2937' }, children: [googleLoading ? ((0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin w-5 h-5", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z" })] })) : ((0, jsx_runtime_1.jsxs)("svg", { className: "w-5 h-5", viewBox: "0 0 24 24", fill: "currentColor", children: [(0, jsx_runtime_1.jsx)("path", { d: "M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z", fill: "#4285F4" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z", fill: "#34A853" }), (0, jsx_runtime_1.jsx)("path", { d: "M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z", fill: "#FBBC05" }), (0, jsx_runtime_1.jsx)("path", { d: "M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z", fill: "#EA4335" })] })), googleLoading ? 'Redirecting to Google…' : 'Continue with Google'] }) })), altAuthSlot && (0, jsx_runtime_1.jsx)("div", { className: "mt-4", children: altAuthSlot }), (0, jsx_runtime_1.jsx)("div", { className: `mt-6 text-center text-sm ${colors.muted}`, children: (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/recovery", className: "themed-link hover:underline", children: "Start account recovery" }) })] }), (0, jsx_runtime_1.jsxs)("p", { className: `mt-4 text-center text-sm ${colors.muted}`, children: ["Don't have an account?", ' ', (0, jsx_runtime_1.jsx)("a", { href: "/account-auth/register", className: "themed-link font-medium hover:underline", children: "Sign up" })] })] }) })] }));
222
205
  }
223
206
  function LoginPageFallback() {
224
207
  const colors = (0, useTheme_1.useColors)();
225
208
  const buttonColor = colors.secondary || colors.primary || '#11B588';
226
209
  return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center ${colors.background}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-10 w-10 mx-auto", style: { color: buttonColor }, xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("p", { className: `mt-4 ${colors.muted}`, children: "Loading..." })] }) }));
227
210
  }
228
- function LoginPage() {
229
- return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(LoginPageFallback, {}), children: (0, jsx_runtime_1.jsx)(LoginForm, {}) }));
211
+ /**
212
+ * @param altAuthSlot an additional sign-in control (e.g. a passkey link)
213
+ * rendered INSIDE the card, after the social row. Optional: omitting it
214
+ * leaves the page byte-identical to before, so existing consumers are
215
+ * unaffected.
216
+ */
217
+ function LoginPage({ altAuthSlot } = {}) {
218
+ return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(LoginPageFallback, {}), children: (0, jsx_runtime_1.jsx)(LoginForm, { altAuthSlot: altAuthSlot }) }));
230
219
  }
@@ -27,4 +27,13 @@
27
27
  * 1. Import from @payez/next-mvp/pages/verify-code
28
28
  * 2. Wrap your app with ThemeProvider to customize branding
29
29
  */
30
- export default function VerifyCodePage(): import("react/jsx-runtime").JSX.Element;
30
+ /**
31
+ * @param enrollHref where a user with NO usable 2FA method is sent to enrol.
32
+ * Defaults to `/account/security`, which is where the enrolment surface
33
+ * lives in the admin graft. It is a PROP rather than a constant because the
34
+ * mount path is the consumer's choice — the package must not assume another
35
+ * app's routing table.
36
+ */
37
+ export default function VerifyCodePage({ enrollHref }?: {
38
+ enrollHref?: string;
39
+ }): import("react/jsx-runtime").JSX.Element;
@@ -42,7 +42,7 @@ const useTheme_1 = require("../../theme/useTheme");
42
42
  * Prevents auto-redirect back to dashboard when session refreshes in background.
43
43
  */
44
44
  const VERIFY_IN_PROGRESS_KEY = 'idealvibe_2fa_verify_in_progress';
45
- function VerifyCodeForm() {
45
+ function VerifyCodeForm({ enrollHref }) {
46
46
  const router = (0, navigation_1.useRouter)();
47
47
  const searchParams = (0, navigation_1.useSearchParams)();
48
48
  const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
@@ -166,6 +166,34 @@ function VerifyCodeForm() {
166
166
  const res = await fetch('/api/session/viability', {
167
167
  credentials: 'include',
168
168
  });
169
+ // ====================================================================
170
+ // THE ENROLMENT DEAD END (cards 170045 / 170047, was 141688 / 141741)
171
+ // ====================================================================
172
+ // A first-time social user arrives here with ZERO usable methods and is
173
+ // asked for a code from an authenticator that does not exist. There was
174
+ // no way out: not a missing feature, AN UNASKED QUESTION.
175
+ //
176
+ // The IDP already decides this and says so. The package already relays
177
+ // it — from BOTH viability exports, deliberately, so an app mounting
178
+ // either is not left blind. The graft already mounts the route. THIS
179
+ // PAGE ALREADY CALLS IT, four lines above. It simply never read the
180
+ // answer: the only branch was `res.status === 401`, so on a perfectly
181
+ // valid session the response was fetched and thrown away.
182
+ //
183
+ // 'enroll_mfa' no USABLE method — the user CANNOT complete a
184
+ // challenge, so keeping them on a code prompt is a trap
185
+ // 'mfa_verify' a usable method exists — the prompt is correct
186
+ // null nothing due
187
+ //
188
+ // Every link in that chain was built against these very cards. The
189
+ // last consumer just never asked.
190
+ if (res.ok) {
191
+ const data = await res.json().catch(() => ({}));
192
+ if (data?.nextStep === 'enroll_mfa') {
193
+ router.replace(enrollHref);
194
+ return;
195
+ }
196
+ }
169
197
  if (res.status === 401) {
170
198
  const data = await res.json().catch(() => ({}));
171
199
  // Session is no longer valid - warn user and redirect
@@ -193,7 +221,40 @@ function VerifyCodeForm() {
193
221
  clearTimeout(initialTimeout);
194
222
  clearInterval(interval);
195
223
  };
196
- }, [status, router]);
224
+ }, [status, router, enrollHref]);
225
+ // ==========================================================================
226
+ // Enrolment dead-end check — IMMEDIATE, and deliberately separate
227
+ // ==========================================================================
228
+ // The poller above first fires after 5 SECONDS. For expiry that is fine. For
229
+ // a user who can never complete this challenge it is five seconds of staring
230
+ // at an unanswerable prompt, which is the whole complaint in card 170045.
231
+ //
232
+ // This runs at once — but it is a SEPARATE effect rather than removing the
233
+ // 5s delay, because that delay guards the 401 path against racing session
234
+ // establishment immediately after sign-in. Firing the expiry logic instantly
235
+ // could sign a user out for having a session that is merely still settling.
236
+ // So: this reads ONLY the 200 body and ONLY routes on 'enroll_mfa'. It never
237
+ // signs anyone out, so it is safe to run with no delay.
238
+ (0, react_1.useEffect)(() => {
239
+ if (status !== 'authenticated')
240
+ return;
241
+ let cancelled = false;
242
+ (async () => {
243
+ try {
244
+ const res = await fetch('/api/session/viability', { credentials: 'include' });
245
+ if (!res.ok || cancelled)
246
+ return;
247
+ const data = await res.json().catch(() => ({}));
248
+ if (data?.nextStep === 'enroll_mfa')
249
+ router.replace(enrollHref);
250
+ }
251
+ catch {
252
+ // Silent: the poller and the next real API call both still cover this.
253
+ // A failed check must leave the user on the prompt, never redirect them.
254
+ }
255
+ })();
256
+ return () => { cancelled = true; };
257
+ }, [status, router, enrollHref]);
197
258
  const handleSendCode = async (selectedMethod) => {
198
259
  setSending(true);
199
260
  setError(null);
@@ -407,6 +468,13 @@ function VerifyCodePageFallback() {
407
468
  const colors = (0, useTheme_1.useColors)();
408
469
  return ((0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-center py-8", style: { background: 'hsl(var(--background))' }, children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-10 w-10 mx-auto", style: { color: 'hsl(var(--primary))' }, xmlns: "http://www.w3.org/2000/svg", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("p", { className: "mt-4", style: { color: 'hsl(var(--muted-foreground))' }, children: "Loading..." })] }) }));
409
470
  }
410
- function VerifyCodePage() {
411
- return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(VerifyCodePageFallback, {}), children: (0, jsx_runtime_1.jsx)(VerifyCodeForm, {}) }));
471
+ /**
472
+ * @param enrollHref where a user with NO usable 2FA method is sent to enrol.
473
+ * Defaults to `/account/security`, which is where the enrolment surface
474
+ * lives in the admin graft. It is a PROP rather than a constant because the
475
+ * mount path is the consumer's choice — the package must not assume another
476
+ * app's routing table.
477
+ */
478
+ function VerifyCodePage({ enrollHref = '/account/security' } = {}) {
479
+ return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(VerifyCodePageFallback, {}), children: (0, jsx_runtime_1.jsx)(VerifyCodeForm, { enrollHref: enrollHref }) }));
412
480
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ready-to-Use Passkey Sign-In Route (ANONYMOUS ceremony)
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkey/sign-in/options/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/auth/passkey-signin';
8
+ * // (optionsPOST is exported as POST; mount verify at its own path:)
9
+ * // app/api/passkey/sign-in/verify/route.ts
10
+ * export { verifyPOST as POST } from '@payez/next-mvp/routes/auth/passkey-signin';
11
+ * ```
12
+ */
13
+ export { optionsPOST, verifyPOST } from '../../api-handlers/auth/passkey-signin';
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.verifyPOST = exports.optionsPOST = void 0;
4
+ /**
5
+ * Ready-to-Use Passkey Sign-In Route (ANONYMOUS ceremony)
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * // app/api/passkey/sign-in/options/route.ts
10
+ * export { POST } from '@payez/next-mvp/routes/auth/passkey-signin';
11
+ * // (optionsPOST is exported as POST; mount verify at its own path:)
12
+ * // app/api/passkey/sign-in/verify/route.ts
13
+ * export { verifyPOST as POST } from '@payez/next-mvp/routes/auth/passkey-signin';
14
+ * ```
15
+ */
16
+ var passkey_signin_1 = require("../../api-handlers/auth/passkey-signin");
17
+ Object.defineProperty(exports, "optionsPOST", { enumerable: true, get: function () { return passkey_signin_1.optionsPOST; } });
18
+ Object.defineProperty(exports, "verifyPOST", { enumerable: true, get: function () { return passkey_signin_1.verifyPOST; } });
@@ -37,6 +37,8 @@ export declare function GET(req: NextRequest): Promise<NextResponse<{
37
37
  tenantRequiresTwoFactor: boolean;
38
38
  userHasCompletedTenantTwoFactorRequirements: any;
39
39
  userStillNeedsTwoFactor: boolean;
40
+ tenantAllowed2FAMethods: string[] | null;
41
+ nextStep: "enroll_mfa" | "mfa_verify" | null | undefined;
40
42
  requires2FA: boolean;
41
43
  twoFactorComplete: any;
42
44
  accessTokenExpired: boolean;