@payez/next-mvp 4.5.6 → 4.7.9

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 (68) hide show
  1. package/dist/api-handlers/auth/credential-login-email-code.d.ts +24 -0
  2. package/dist/api-handlers/auth/credential-login-email-code.js +83 -0
  3. package/dist/api-handlers/auth/credential-login.d.ts +41 -0
  4. package/dist/api-handlers/auth/credential-login.js +858 -0
  5. package/dist/api-handlers/oauth/resume.d.ts +16 -0
  6. package/dist/api-handlers/oauth/resume.js +207 -0
  7. package/dist/auth/better-auth.d.ts +14 -2
  8. package/dist/auth/better-auth.js +59 -34
  9. package/dist/auth/idp-oauth.d.ts +106 -0
  10. package/dist/auth/idp-oauth.js +280 -0
  11. package/dist/auth/safe-callback.js +15 -2
  12. package/dist/auth/session-cookie-clear.d.ts +7 -0
  13. package/dist/auth/session-cookie-clear.js +24 -0
  14. package/dist/auth/session-reset-link.d.ts +43 -0
  15. package/dist/auth/session-reset-link.js +144 -0
  16. package/dist/components/auth/CredentialSignInForm.d.ts +33 -0
  17. package/dist/components/auth/CredentialSignInForm.js +134 -0
  18. package/dist/lib/authorize-resume.d.ts +87 -0
  19. package/dist/lib/authorize-resume.js +190 -0
  20. package/dist/lib/bearer-fetch.d.ts +17 -0
  21. package/dist/lib/bearer-fetch.js +32 -0
  22. package/dist/lib/code-step-copy.d.ts +53 -0
  23. package/dist/lib/code-step-copy.js +73 -0
  24. package/dist/lib/credential-sign-in-client.d.ts +59 -0
  25. package/dist/lib/credential-sign-in-client.js +89 -0
  26. package/dist/lib/device-id.d.ts +37 -0
  27. package/dist/lib/device-id.js +70 -0
  28. package/dist/lib/lock-window.d.ts +29 -0
  29. package/dist/lib/lock-window.js +53 -0
  30. package/dist/lib/login-error-message.d.ts +18 -0
  31. package/dist/lib/login-error-message.js +47 -0
  32. package/dist/lib/twofa-rejection.d.ts +20 -0
  33. package/dist/lib/twofa-rejection.js +73 -0
  34. package/dist/middleware/create-middleware.d.ts +25 -1
  35. package/dist/middleware/create-middleware.js +75 -27
  36. package/dist/pages/login/page.js +11 -37
  37. package/dist/routes/auth/credential-login-email-code.d.ts +6 -0
  38. package/dist/routes/auth/credential-login-email-code.js +10 -0
  39. package/dist/routes/auth/credential-login.d.ts +6 -0
  40. package/dist/routes/auth/credential-login.js +10 -0
  41. package/dist/routes/auth/session-reset.d.ts +21 -0
  42. package/dist/routes/auth/session-reset.js +40 -0
  43. package/dist/routes/oauth/resume.d.ts +11 -0
  44. package/dist/routes/oauth/resume.js +16 -0
  45. package/package.json +1102 -1042
  46. package/src/api-handlers/auth/credential-login-email-code.ts +92 -0
  47. package/src/api-handlers/auth/credential-login.ts +948 -0
  48. package/src/api-handlers/oauth/resume.ts +182 -0
  49. package/src/auth/better-auth.ts +81 -5
  50. package/src/auth/idp-oauth.ts +310 -0
  51. package/src/auth/safe-callback.ts +16 -2
  52. package/src/auth/session-cookie-clear.ts +30 -0
  53. package/src/auth/session-reset-link.ts +159 -0
  54. package/src/components/auth/CredentialSignInForm.tsx +240 -0
  55. package/src/lib/authorize-resume.ts +187 -0
  56. package/src/lib/bearer-fetch.ts +31 -0
  57. package/src/lib/code-step-copy.ts +83 -0
  58. package/src/lib/credential-sign-in-client.ts +101 -0
  59. package/src/lib/device-id.ts +67 -0
  60. package/src/lib/lock-window.ts +49 -0
  61. package/src/lib/login-error-message.ts +41 -0
  62. package/src/lib/twofa-rejection.ts +91 -0
  63. package/src/middleware/create-middleware.ts +124 -30
  64. package/src/pages/login/page.tsx +20 -34
  65. package/src/routes/auth/credential-login-email-code.ts +6 -0
  66. package/src/routes/auth/credential-login.ts +6 -0
  67. package/src/routes/auth/session-reset.ts +37 -0
  68. package/src/routes/oauth/resume.ts +11 -0
@@ -28,6 +28,8 @@ const safe_callback_1 = require("../auth/safe-callback");
28
28
  const internal_api_url_1 = require("../edge/internal-api-url");
29
29
  const app_slug_1 = require("../lib/app-slug");
30
30
  const rbac_check_1 = require("./rbac-check");
31
+ const session_cookie_clear_1 = require("../auth/session-cookie-clear");
32
+ const session_reset_link_1 = require("../auth/session-reset-link");
31
33
  // =============================================================================
32
34
  // DEFAULT IMPLEMENTATIONS
33
35
  // =============================================================================
@@ -58,8 +60,12 @@ const defaultLogger = {
58
60
  // =============================================================================
59
61
  const LOGIN_PAGE = '/account-auth/login';
60
62
  const VERIFY_CODE_PAGE = '/account-auth/verify-code';
63
+ // PAY-2315 (4.7.1): /login is a public alias of the login page. It used to take the 'public: allow' arm and never reached the
64
+ // 'already_authenticated' redirect, so a signed-in user was shown the sign-in form. It is the login page for every arm now.
65
+ // (safe-callback refuses /login as a destination, so this cannot redirect to itself.)
66
+ const LOGIN_ALIAS = '/login';
61
67
  function isLoginPage(pathname) {
62
- return pathname === LOGIN_PAGE;
68
+ return pathname === LOGIN_PAGE || pathname === LOGIN_ALIAS;
63
69
  }
64
70
  /**
65
71
  * Ensures callback URLs never point to auth pages (prevents redirect loops) AND
@@ -105,24 +111,7 @@ function isPrefetchRequest(request) {
105
111
  function clearSessionCookies(response, request) {
106
112
  if (isPrefetchRequest(request))
107
113
  return;
108
- const sessionCookie = (0, app_slug_1.getSessionCookieName)();
109
- const secureCookie = (0, app_slug_1.getSecureSessionCookieName)();
110
- // Clear main cookies
111
- response.cookies.set(sessionCookie, '', { path: '/', expires: new Date(0), httpOnly: true, sameSite: 'lax' });
112
- response.cookies.set(secureCookie, '', { path: '/', expires: new Date(0), httpOnly: true, sameSite: 'lax', secure: true });
113
- // Clear any chunked cookies (NextAuth chunks large cookies)
114
- for (const [name] of request.cookies) {
115
- if (name.startsWith(`${sessionCookie}.`) || name.startsWith(`${secureCookie}.`)) {
116
- const isSecure = name.startsWith('__Secure-');
117
- response.cookies.set(name, '', {
118
- path: '/',
119
- expires: new Date(0),
120
- httpOnly: true,
121
- sameSite: 'lax',
122
- ...(isSecure && { secure: true })
123
- });
124
- }
125
- }
114
+ (0, session_cookie_clear_1.expireSessionCookies)(response, Array.from(request.cookies, ([name]) => name));
126
115
  }
127
116
  /** Create redirect response, optionally clearing cookies */
128
117
  function redirectTo(request, location, clearCookies = false) {
@@ -132,6 +121,28 @@ function redirectTo(request, location, clearCookies = false) {
132
121
  }
133
122
  return response;
134
123
  }
124
+ /**
125
+ * A redirect that must also END the session. With the reset route configured, a page request is sent through it
126
+ * WITHOUT any Set-Cookie here: the route sees the flight headers this middleware cannot (Next strips them before
127
+ * middleware), answers a prefetch 204 + no-store, and clears on the real navigation. `/api/` requests and apps
128
+ * without the route keep the direct clear exactly as before.
129
+ */
130
+ async function clearingRedirect(request, location, reset) {
131
+ if (!reset || request.nextUrl.pathname.startsWith('/api/')) {
132
+ return redirectTo(request, location, true);
133
+ }
134
+ const target = new URL(location, request.url);
135
+ const params = (0, session_reset_link_1.normaliseResetParams)({
136
+ callbackUrl: target.searchParams.get('callbackUrl'),
137
+ reason: target.searchParams.get('reason'),
138
+ error: target.searchParams.get('error'),
139
+ });
140
+ // Bound to THIS request's session cookie: the route clears only when its own request carries the same one.
141
+ const bound = (0, session_reset_link_1.boundSessionCookie)((n) => request.cookies.get(n)?.value, (0, app_slug_1.getSessionCookieName)(), (0, app_slug_1.getSecureSessionCookieName)());
142
+ const response = server_1.NextResponse.redirect(new URL(await (0, session_reset_link_1.signedResetHref)(reset.route, params, reset.secret, bound), request.url));
143
+ response.headers.set('cache-control', 'no-store');
144
+ return response;
145
+ }
135
146
  /**
136
147
  * Terminal response for a FAILED token refresh.
137
148
  *
@@ -150,7 +161,7 @@ function redirectTo(request, location, clearCookies = false) {
150
161
  * higher would intercept 'refresh_needed' and disable middleware token refresh across
151
162
  * the whole API surface (card 340024 C1).
152
163
  */
153
- function refreshFailureResponse(request, location, errorCode, clearCookies = false) {
164
+ async function refreshFailureResponse(request, location, errorCode, clearCookies = false, reset) {
154
165
  if (request.nextUrl.pathname.startsWith('/api/')) {
155
166
  const response = server_1.NextResponse.json({ error: errorCode }, { status: 401 });
156
167
  if (clearCookies) {
@@ -158,7 +169,7 @@ function refreshFailureResponse(request, location, errorCode, clearCookies = fal
158
169
  }
159
170
  return response;
160
171
  }
161
- return redirectTo(request, location, clearCookies);
172
+ return clearCookies ? clearingRedirect(request, location, reset) : redirectTo(request, location);
162
173
  }
163
174
  /**
164
175
  * PAY-2147: the terminal answer when the middleware CANNOT TELL whether the session is good
@@ -359,6 +370,8 @@ function redirect2FA(callbackUrl) {
359
370
  // =============================================================================
360
371
  // MIDDLEWARE FACTORY
361
372
  // =============================================================================
373
+ /** PAY-2218: the stripped-flight-headers warning is logged once per process, not once per request. */
374
+ let flightHeadersStrippedWarned = false;
362
375
  function createMvpMiddleware(options = {}) {
363
376
  // Resolve options with defaults
364
377
  const cb = options.circuitBreaker || defaultCircuitBreaker;
@@ -369,6 +382,29 @@ function createMvpMiddleware(options = {}) {
369
382
  const rbacExemptPaths = options.rbacExemptPaths || [];
370
383
  // PAY-1882 step 4: default 'closed' (Jon's option A / BAPert 66316).
371
384
  const revocationFailMode = options.revocationFailMode || 'closed';
385
+ // PAY-2302 cut 1c: strictly `=== true`, so a truthy string in a config file cannot switch revocation off.
386
+ const noRevocationSource = options.noRevocationSource === true;
387
+ if (noRevocationSource) {
388
+ log.warn('[next-mvp] noRevocationSource is ON: sessions of this app CANNOT be revoked server-side (no vibe client / HMAC key). ' +
389
+ "revocationStatus 'not_configured' is treated as viable; 'unknown' and 'revoked' still sign the user out. " +
390
+ 'Remove this option once the environment has a vibe client id and HMAC key.');
391
+ }
392
+ const sessionResetRoute = options.sessionResetRoute === true ? session_reset_link_1.DEFAULT_SESSION_RESET_ROUTE
393
+ : typeof options.sessionResetRoute === 'string' && options.sessionResetRoute ? options.sessionResetRoute
394
+ : undefined;
395
+ let resetSecretMissingLogged = false;
396
+ const resolveReset = () => {
397
+ if (!sessionResetRoute)
398
+ return undefined;
399
+ const secret = (0, session_reset_link_1.resetSecret)();
400
+ if (secret)
401
+ return { route: sessionResetRoute, secret };
402
+ if (!resetSecretMissingLogged) {
403
+ resetSecretMissingLogged = true;
404
+ log.error('sessionResetRoute is set but no BETTER_AUTH_SECRET/NEXTAUTH_SECRET is available to sign the hand-off; clearing in middleware instead', { sessionResetRoute });
405
+ }
406
+ return undefined;
407
+ };
372
408
  return async function middleware(request) {
373
409
  const { pathname, searchParams } = request.nextUrl;
374
410
  // =========================================================================
@@ -387,6 +423,12 @@ function createMvpMiddleware(options = {}) {
387
423
  return server_1.NextResponse.next();
388
424
  }
389
425
  }
426
+ // PAY-2218: a router request (it carries `next-url`) that arrives WITHOUT `rsc` had its flight headers stripped
427
+ // by Next before middleware ran, so isPrefetchRequest() cannot see a router prefetch on this app. Say so once.
428
+ if (!sessionResetRoute && !flightHeadersStrippedWarned && request.headers.has('next-url') && !request.headers.has('rsc')) {
429
+ flightHeadersStrippedWarned = true;
430
+ log.warn('Next strips router prefetch headers before middleware on this app, so a session-ending redirect can fire on a prefetch. Mount @payez/next-mvp/routes/auth/session-reset and set sessionResetRoute (PAY-2218).', { pathname });
431
+ }
390
432
  const isPublic = (0, route_config_1.isUnauthenticatedRoute)(pathname);
391
433
  const isLogin = isLoginPage(pathname);
392
434
  // --- Optimization: Skip viability check for unauthenticated public browsing ---
@@ -399,7 +441,7 @@ function createMvpMiddleware(options = {}) {
399
441
  // Has cookie - continue to viability check for 2FA enforcement
400
442
  }
401
443
  // --- Fetch session viability from internal API ---
402
- const { sessionStatus, sessionPointer } = await checkViability(request, viabilityEndpoint, log, revocationFailMode);
444
+ const { sessionStatus, sessionPointer } = await checkViability(request, viabilityEndpoint, log, revocationFailMode, noRevocationSource);
403
445
  // --- Stale session (cookie exists but session doesn't): OBSERVE ONLY, DO NOT DECIDE ---
404
446
  //
405
447
  // *** CARD 349607. THIS BLOCK USED TO `return` HERE, AND THAT RETURN WAS A BYPASS. ***
@@ -456,6 +498,7 @@ function createMvpMiddleware(options = {}) {
456
498
  logger: log,
457
499
  refreshEndpoint,
458
500
  rbacExemptPaths,
501
+ reset: resolveReset(),
459
502
  onRefreshSuccess: options.onRefreshSuccess,
460
503
  onRefreshFailure: options.onRefreshFailure,
461
504
  });
@@ -473,13 +516,16 @@ function createMvpMiddleware(options = {}) {
473
516
  * 'unknown'/'not_configured'. The booleans are read ONLY when revocationStatus is
474
517
  * absent (idealresume.online's older boolean-only route).
475
518
  */
476
- function decideForceInvalid(data, revocationFailMode) {
519
+ function decideForceInvalid(data, revocationFailMode, noRevocationSource = false) {
477
520
  const status = data.revocationStatus;
478
521
  if (typeof status === 'string') {
479
522
  if (status === 'revoked')
480
523
  return true;
481
524
  if (status === 'active')
482
525
  return false;
526
+ // PAY-2302 cut 1c: an app that declared it has no revocation source. ONLY the exact 'not_configured' string.
527
+ if (status === 'not_configured' && noRevocationSource === true)
528
+ return false;
483
529
  // S1 (NightHawk 66409): ANY other string - 'unknown', 'not_configured', a
484
530
  // typo like 'Revoked', 'error', or '' - is treated as 'unknown' (fail mode
485
531
  // applies), NEVER as 'active'. Returning false here would let the first
@@ -502,7 +548,7 @@ function hasSessionCookie(request) {
502
548
  return !!(request.cookies.get((0, app_slug_1.getSessionCookieName)())?.value || request.cookies.get((0, app_slug_1.getSecureSessionCookieName)())?.value);
503
549
  }
504
550
  /** Check session viability via internal API */
505
- async function checkViability(request, endpoint, log, revocationFailMode) {
551
+ async function checkViability(request, endpoint, log, revocationFailMode, noRevocationSource = false) {
506
552
  const sessionStatus = {
507
553
  exists: false,
508
554
  forceInvalid: false,
@@ -574,7 +620,7 @@ async function checkViability(request, endpoint, log, revocationFailMode) {
574
620
  // revocations status, decide from it + failMode and ignore the folded
575
621
  // booleans for unknown/not_configured. The booleans are read ONLY when
576
622
  // revocationStatus is ABSENT (idealresume.online's older boolean route).
577
- sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode);
623
+ sessionStatus.forceInvalid = decideForceInvalid(data, revocationFailMode, noRevocationSource);
578
624
  // PAY-2216: a STALE session - the cookie resolved to a token, but no store holds the session - used to set
579
625
  // pointer.exists=false, so makeAuthDecision took no_session (login, cookie NOT cleared) and its stale_session
580
626
  // arm (clearCookies) was unreachable: a dead cookie lingered and re-ran viability on every request.
@@ -613,7 +659,9 @@ async function executeDecision(request, decision, pathname, sessionPointer, sess
613
659
  case 'allow':
614
660
  return handleAllow(request, pathname, sessionPointer, sessionStatus, opts.rbacExemptPaths);
615
661
  case 'redirect':
616
- return redirectTo(request, decision.location, decision.clearCookies);
662
+ return decision.clearCookies
663
+ ? clearingRedirect(request, decision.location, opts.reset)
664
+ : redirectTo(request, decision.location);
617
665
  case 'service_error':
618
666
  return serviceUnavailableResponse(request);
619
667
  case 'refresh_needed':
@@ -719,7 +767,7 @@ async function handleRefresh(request, safeCallback, opts) {
719
767
  clearingCookies: isPermanentFailure
720
768
  });
721
769
  onRefreshFailure?.(response.status, false);
722
- return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, errorParam, isPermanentFailure);
770
+ return refreshFailureResponse(request, `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=${errorParam}`, errorParam, isPermanentFailure, opts.reset);
723
771
  }
724
772
  catch (error) {
725
773
  const isNetwork = cb.isNetworkError(error);
@@ -29,6 +29,7 @@ const better_auth_client_1 = require("../../client/better-auth-client");
29
29
  const navigation_1 = require("next/navigation");
30
30
  const react_2 = require("react");
31
31
  const useTheme_1 = require("../../theme/useTheme");
32
+ const login_error_message_1 = require("../../lib/login-error-message");
32
33
  function LoginForm({ altAuthSlot }) {
33
34
  const searchParams = (0, navigation_1.useSearchParams)();
34
35
  const callbackUrl = searchParams?.get('callbackUrl') || '/dashboard';
@@ -110,46 +111,19 @@ function LoginForm({ altAuthSlot }) {
110
111
  // Atomic state management - only ONE can be true at a time
111
112
  const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
112
113
  const [loginSuccess, setLoginSuccess] = (0, react_1.useState)(false);
113
- const [loginError, setLoginError] = (0, react_1.useState)(null);
114
+ // undefined = the user has not acted yet (show what the URL says); null/string = an explicit state set by a handler below.
115
+ const [loginError, setLoginError] = (0, react_1.useState)(undefined);
114
116
  const [loading, setLoading] = (0, react_1.useState)(false);
115
117
  const [showRecoveryOptions, setShowRecoveryOptions] = (0, react_1.useState)(false);
116
118
  // Force show form after timeout if stuck in loading
117
119
  const [forceShowForm, setForceShowForm] = (0, react_1.useState)(false);
118
- // Surface OAuth failures the Better Auth mount bounced back here.
120
+ // PAY-2319 (4.7.2): what the URL says, derived at render (no effect) so it is shown on the FIRST paint and in a static render.
121
+ // 4.7.1 mapped these in effects that only set loginError, which was never rendered. Unknown codes are NOT echoed (see lib/login-error-message).
122
+ const urlMessage = (0, login_error_message_1.oauthFailureMessage)(oauthError) ?? (0, login_error_message_1.friendlyLoginError)(urlError);
123
+ const shownError = loginError !== undefined ? loginError : urlMessage;
119
124
  (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]);
125
- // Handle URL errors with user-friendly messages
126
- (0, react_1.useEffect)(() => {
127
- if (urlError) {
128
- let friendlyError = urlError;
129
- if (urlError === 'SessionExpired') {
130
- // Session expiration is normal - don't show as error
131
- console.log('[LOGIN] SessionExpired detected - normal behavior');
132
- friendlyError = null;
133
- }
134
- else if (urlError === 'TwoFactorTimeout') {
135
- // PAY-884: the sign-in expired while the verification code was outstanding. Unlike
136
- // SessionExpired this is NOT silent - the user was mid-2FA and must know why they are here.
137
- friendlyError = 'Your verification step timed out before the code was confirmed. Please sign in again.';
138
- }
139
- else if (urlError === 'ServiceUnavailable') {
140
- friendlyError = 'Authentication service is temporarily unavailable. Please try again in a few minutes.';
141
- }
142
- else if (urlError.startsWith("Unexpected token")) {
143
- friendlyError = 'An authentication error occurred. Please try signing in again.';
144
- }
145
- else if (urlError.includes('not valid JSON')) {
146
- friendlyError = 'An authentication error occurred. Please try signing in again.';
147
- }
148
- else if (urlError === 'CredentialsSignin') {
149
- friendlyError = 'Invalid email or password. Please try again.';
150
- }
151
- setLoginError(friendlyError);
152
- }
125
+ if (urlError === 'SessionExpired')
126
+ console.log('[LOGIN] SessionExpired detected - normal behavior');
153
127
  }, [urlError]);
154
128
  // Timeout to prevent infinite loading states
155
129
  (0, react_1.useEffect)(() => {
@@ -175,7 +149,7 @@ function LoginForm({ altAuthSlot }) {
175
149
  // authClient.signIn.email, which better-auth always rejected because this
176
150
  // package never configures emailAndPassword. See the note at the render site.
177
151
  // Show loading spinner for initial loads
178
- if (!forceShowForm && !loginError && (loading || (status === 'loading' && !session))) {
152
+ if (!forceShowForm && !shownError && (loading || (status === 'loading' && !session))) {
179
153
  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...' })] }));
180
154
  }
181
155
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: `
@@ -206,7 +180,7 @@ function LoginForm({ altAuthSlot }) {
206
180
  .themed-status-submitting svg {
207
181
  color: ${buttonColor} !important;
208
182
  }
209
- ` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsx)("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" }) })] }) }) })] }));
183
+ ` } }), (0, jsx_runtime_1.jsx)("div", { className: `min-h-screen flex items-center justify-center px-4 ${colors.background}`, children: (0, jsx_runtime_1.jsx)("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" })] }), shownError && ((0, jsx_runtime_1.jsx)("div", { role: "alert", "data-testid": "login-error", className: "mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700", children: shownError })), 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" }) })] }) }) })] }));
210
184
  }
211
185
  function LoginPageFallback() {
212
186
  const colors = (0, useTheme_1.useColors)();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * PAY-2302 cut 1: ask the IdP to email a code when the authenticator lane failed on our side.
3
+ * The handler lives in api-handlers/auth/credential-login-email-code.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/email-code/route.ts). A consuming app's route file
4
+ * re-exports POST and writes `export const runtime = 'nodejs'` literally.
5
+ */
6
+ export { POST } from '../../api-handlers/auth/credential-login-email-code';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = void 0;
4
+ /**
5
+ * PAY-2302 cut 1: ask the IdP to email a code when the authenticator lane failed on our side.
6
+ * The handler lives in api-handlers/auth/credential-login-email-code.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/email-code/route.ts). A consuming app's route file
7
+ * re-exports POST and writes `export const runtime = 'nodejs'` literally.
8
+ */
9
+ var credential_login_email_code_1 = require("../../api-handlers/auth/credential-login-email-code");
10
+ Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return credential_login_email_code_1.POST; } });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * PAY-2302 cut 1: password sign-in route (IdP ExternalAuth/login, session written via lib/session-store, 2FA-outstanding handling).
3
+ * The handler lives in api-handlers/auth/credential-login.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/route.ts). A consuming app's route file
4
+ * re-exports POST and writes `export const runtime = 'nodejs'` literally.
5
+ */
6
+ export { POST } from '../../api-handlers/auth/credential-login';
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.POST = void 0;
4
+ /**
5
+ * PAY-2302 cut 1: password sign-in route (IdP ExternalAuth/login, session written via lib/session-store, 2FA-outstanding handling).
6
+ * The handler lives in api-handlers/auth/credential-login.ts (moved from VibeID-Customer-Portal src/app/api/auth/admin-login/route.ts). A consuming app's route file
7
+ * re-exports POST and writes `export const runtime = 'nodejs'` literally.
8
+ */
9
+ var credential_login_1 = require("../../api-handlers/auth/credential-login");
10
+ Object.defineProperty(exports, "POST", { enumerable: true, get: function () { return credential_login_1.POST; } });
@@ -0,0 +1,21 @@
1
+ /**
2
+ * PAY-2218 PR 2: the session-reset route. Mount it at app/api/auth/session-reset/route.ts:
3
+ *
4
+ * export { GET } from '@payez/next-mvp/routes/auth/session-reset';
5
+ *
6
+ * and turn it on with createMvpMiddleware({ sessionResetRoute: true }). The middleware then sends a page's
7
+ * session-ending redirect (stale session, force-invalidated, pre-2FA timeout, refresh 401/403) here instead of
8
+ * clearing the cookies itself. See src/auth/session-reset-link.ts for the measurements behind each rule.
9
+ *
10
+ * - Router PREFETCH (the route sees `next-router-prefetch`, which Next strips before middleware): 204 + no-store,
11
+ * no redirect, no Set-Cookie. A redirect here would be cached by the 15.5.x router and reused by the click, so
12
+ * the real navigation would never come back to clear.
13
+ * - Real navigation with a valid signed hand-off: expire the session cookies and 307 to the login page.
14
+ * - Anything unsigned, tampered, expired, or bound to a DIFFERENT session than this request's cookie: 307 to the
15
+ * login page and touch NO cookie (nobody can link a signed-in user here to log them out; a second use is a no-op).
16
+ *
17
+ * The login URL is always rebuilt from allowlisted parts; the route never redirects to a URL it was given.
18
+ */
19
+ import { NextRequest, NextResponse } from 'next/server';
20
+ export declare const dynamic = "force-dynamic";
21
+ export declare function GET(request: NextRequest): Promise<NextResponse>;
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.dynamic = void 0;
4
+ exports.GET = GET;
5
+ /**
6
+ * PAY-2218 PR 2: the session-reset route. Mount it at app/api/auth/session-reset/route.ts:
7
+ *
8
+ * export { GET } from '@payez/next-mvp/routes/auth/session-reset';
9
+ *
10
+ * and turn it on with createMvpMiddleware({ sessionResetRoute: true }). The middleware then sends a page's
11
+ * session-ending redirect (stale session, force-invalidated, pre-2FA timeout, refresh 401/403) here instead of
12
+ * clearing the cookies itself. See src/auth/session-reset-link.ts for the measurements behind each rule.
13
+ *
14
+ * - Router PREFETCH (the route sees `next-router-prefetch`, which Next strips before middleware): 204 + no-store,
15
+ * no redirect, no Set-Cookie. A redirect here would be cached by the 15.5.x router and reused by the click, so
16
+ * the real navigation would never come back to clear.
17
+ * - Real navigation with a valid signed hand-off: expire the session cookies and 307 to the login page.
18
+ * - Anything unsigned, tampered, expired, or bound to a DIFFERENT session than this request's cookie: 307 to the
19
+ * login page and touch NO cookie (nobody can link a signed-in user here to log them out; a second use is a no-op).
20
+ *
21
+ * The login URL is always rebuilt from allowlisted parts; the route never redirects to a URL it was given.
22
+ */
23
+ const server_1 = require("next/server");
24
+ const create_middleware_1 = require("../../middleware/create-middleware");
25
+ const session_cookie_clear_1 = require("../../auth/session-cookie-clear");
26
+ const app_slug_1 = require("../../lib/app-slug");
27
+ const session_reset_link_1 = require("../../auth/session-reset-link");
28
+ exports.dynamic = 'force-dynamic';
29
+ async function GET(request) {
30
+ if ((0, create_middleware_1.isPrefetchRequest)(request)) {
31
+ return new server_1.NextResponse(null, { status: 204, headers: { 'cache-control': 'no-store' } });
32
+ }
33
+ const bound = (0, session_reset_link_1.boundSessionCookie)((n) => request.cookies.get(n)?.value, (0, app_slug_1.getSessionCookieName)(), (0, app_slug_1.getSecureSessionCookieName)());
34
+ const { params, signed } = await (0, session_reset_link_1.readResetRequest)(request.nextUrl.searchParams, (0, session_reset_link_1.resetSecret)(), bound);
35
+ const response = server_1.NextResponse.redirect(new URL((0, session_reset_link_1.loginLocation)(params), request.url), 307);
36
+ response.headers.set('cache-control', 'no-store');
37
+ if (signed)
38
+ (0, session_cookie_clear_1.expireSessionCookies)(response, Array.from(request.cookies, ([name]) => name));
39
+ return response;
40
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * PAY-2302 cut 2: post-2FA OAuth authorize resume route. The handler lives in api-handlers/oauth/resume.ts
3
+ * (moved from VibeID-Customer-Portal src/app/api/oauth/resume/route.ts). A consuming app's route file re-exports GET
4
+ * and writes `export const runtime = 'nodejs'` literally.
5
+ *
6
+ * 4.7.3: `createOAuthResumeHandler({ refusalLocation })` and `DEFAULT_REFUSAL_LOCATION` are re-exported HERE because a consumer can only
7
+ * import what `package.json` `exports` lists, and this route module is the entry. An app whose sign-in page is not
8
+ * /account-auth/login (the portal uses /sign-in) writes `export const GET = createOAuthResumeHandler({ refusalLocation: '/sign-in?error=AUTHORIZE_RESUME_REFUSED' });`.
9
+ */
10
+ export { GET, createOAuthResumeHandler, DEFAULT_REFUSAL_LOCATION } from '../../api-handlers/oauth/resume';
11
+ export type { OAuthResumeOptions, OAuthResumeDeps } from '../../api-handlers/oauth/resume';
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REFUSAL_LOCATION = exports.createOAuthResumeHandler = exports.GET = void 0;
4
+ /**
5
+ * PAY-2302 cut 2: post-2FA OAuth authorize resume route. The handler lives in api-handlers/oauth/resume.ts
6
+ * (moved from VibeID-Customer-Portal src/app/api/oauth/resume/route.ts). A consuming app's route file re-exports GET
7
+ * and writes `export const runtime = 'nodejs'` literally.
8
+ *
9
+ * 4.7.3: `createOAuthResumeHandler({ refusalLocation })` and `DEFAULT_REFUSAL_LOCATION` are re-exported HERE because a consumer can only
10
+ * import what `package.json` `exports` lists, and this route module is the entry. An app whose sign-in page is not
11
+ * /account-auth/login (the portal uses /sign-in) writes `export const GET = createOAuthResumeHandler({ refusalLocation: '/sign-in?error=AUTHORIZE_RESUME_REFUSED' });`.
12
+ */
13
+ var resume_1 = require("../../api-handlers/oauth/resume");
14
+ Object.defineProperty(exports, "GET", { enumerable: true, get: function () { return resume_1.GET; } });
15
+ Object.defineProperty(exports, "createOAuthResumeHandler", { enumerable: true, get: function () { return resume_1.createOAuthResumeHandler; } });
16
+ Object.defineProperty(exports, "DEFAULT_REFUSAL_LOCATION", { enumerable: true, get: function () { return resume_1.DEFAULT_REFUSAL_LOCATION; } });