@payez/next-mvp 4.12.1 → 4.13.1

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 (115) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/api-handlers/account/profile.d.ts +2 -39
  3. package/dist/api-handlers/account/profile.js +5 -2
  4. package/dist/api-handlers/anon/preferences.d.ts +1 -14
  5. package/dist/api-handlers/anon/preferences.js +5 -5
  6. package/dist/api-handlers/auth/credential-login.js +28 -4
  7. package/dist/api-handlers/health.d.ts +5 -0
  8. package/dist/api-handlers/health.js +48 -0
  9. package/dist/api-handlers/mfa/totp-confirm.js +8 -4
  10. package/dist/api-handlers/mfa/totp-enroll.js +8 -4
  11. package/dist/api-handlers/session/refresh-viability.d.ts +3 -15
  12. package/dist/api-handlers/session/refresh-viability.js +5 -2
  13. package/dist/api-handlers/session/viability.d.ts +3 -8
  14. package/dist/api-handlers/session/viability.js +11 -4
  15. package/dist/auth/better-auth-exposure.d.ts +11 -0
  16. package/dist/auth/better-auth-exposure.js +73 -0
  17. package/dist/auth/better-auth.d.ts +6 -0
  18. package/dist/auth/better-auth.js +42 -3
  19. package/dist/auth/route-config.js +9 -0
  20. package/dist/auth/session-reset-link.d.ts +32 -0
  21. package/dist/auth/session-reset-link.js +45 -1
  22. package/dist/components/auth/CredentialSignInForm.d.ts +7 -1
  23. package/dist/components/auth/CredentialSignInForm.js +13 -4
  24. package/dist/components/auth/EnrollAuthenticatorOffer.d.ts +21 -0
  25. package/dist/components/auth/EnrollAuthenticatorOffer.js +23 -0
  26. package/dist/components/mfa/QrCode.d.ts +6 -0
  27. package/dist/components/mfa/QrCode.js +14 -0
  28. package/dist/components/mfa/TotpEnrollPanel.d.ts +6 -0
  29. package/dist/components/mfa/TotpEnrollPanel.js +117 -0
  30. package/dist/edge/middleware.d.ts +1 -1
  31. package/dist/edge/middleware.js +2 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +2 -1
  34. package/dist/lib/credential-sign-in-client.d.ts +26 -1
  35. package/dist/lib/credential-sign-in-client.js +32 -6
  36. package/dist/lib/idp-client-config.js +4 -1
  37. package/dist/lib/login-error-message.d.ts +3 -0
  38. package/dist/lib/login-error-message.js +12 -1
  39. package/dist/lib/mfa-json-guard.d.ts +12 -0
  40. package/dist/lib/mfa-json-guard.js +19 -0
  41. package/dist/lib/nextauth-secret.js +3 -1
  42. package/dist/lib/no-store.d.ts +13 -0
  43. package/dist/lib/no-store.js +35 -0
  44. package/dist/lib/payez-client-secret.d.ts +14 -0
  45. package/dist/lib/payez-client-secret.js +41 -0
  46. package/dist/lib/qrcode.d.ts +22 -0
  47. package/dist/lib/qrcode.js +362 -0
  48. package/dist/lib/sign-in-failure.d.ts +35 -0
  49. package/dist/lib/sign-in-failure.js +85 -0
  50. package/dist/lib/twofa-rejection.d.ts +2 -0
  51. package/dist/lib/twofa-rejection.js +5 -2
  52. package/dist/middleware/create-middleware.d.ts +24 -0
  53. package/dist/middleware/create-middleware.js +56 -9
  54. package/dist/middleware/prefetch-guard-status.d.ts +21 -0
  55. package/dist/middleware/prefetch-guard-status.js +42 -0
  56. package/dist/pages/enroll/page.d.ts +7 -0
  57. package/dist/pages/enroll/page.js +15 -0
  58. package/dist/pages/login/page.js +1 -1
  59. package/dist/routes/auth/session-reset.d.ts +11 -1
  60. package/dist/routes/auth/session-reset.js +38 -13
  61. package/dist/routes/auth/session.d.ts +2 -9
  62. package/dist/routes/auth/session.js +5 -2
  63. package/dist/routes/auth/viability.d.ts +3 -54
  64. package/dist/routes/auth/viability.js +16 -4
  65. package/dist/routes/health.d.ts +1 -0
  66. package/dist/routes/health.js +16 -0
  67. package/dist/routes/mfa/methods.d.ts +2 -2
  68. package/dist/routes/mfa/methods.js +3 -1
  69. package/dist/routes/passkeys/list.d.ts +2 -10
  70. package/dist/routes/passkeys/list.js +4 -2
  71. package/dist/routes/session/refresh-viability.d.ts +2 -1
  72. package/dist/routes/session/refresh-viability.js +4 -2
  73. package/dist/utils/api.d.ts +33 -6
  74. package/dist/utils/api.js +35 -2
  75. package/package.json +21 -1
  76. package/src/api-handlers/account/profile.ts +5 -1
  77. package/src/api-handlers/anon/preferences.ts +5 -4
  78. package/src/api-handlers/auth/credential-login.ts +29 -4
  79. package/src/api-handlers/health.ts +53 -0
  80. package/src/api-handlers/mfa/totp-confirm.ts +7 -4
  81. package/src/api-handlers/mfa/totp-enroll.ts +7 -4
  82. package/src/api-handlers/session/refresh-viability.ts +5 -1
  83. package/src/api-handlers/session/viability.ts +12 -4
  84. package/src/auth/better-auth-exposure.ts +69 -0
  85. package/src/auth/better-auth.ts +42 -3
  86. package/src/auth/route-config.ts +9 -0
  87. package/src/auth/session-reset-link.ts +47 -0
  88. package/src/components/auth/CredentialSignInForm.tsx +21 -3
  89. package/src/components/auth/EnrollAuthenticatorOffer.tsx +48 -0
  90. package/src/components/mfa/QrCode.tsx +24 -0
  91. package/src/components/mfa/TotpEnrollPanel.tsx +203 -0
  92. package/src/edge/middleware.ts +1 -1
  93. package/src/index.ts +1 -1
  94. package/src/lib/credential-sign-in-client.ts +48 -8
  95. package/src/lib/idp-client-config.ts +5 -1
  96. package/src/lib/login-error-message.ts +13 -1
  97. package/src/lib/mfa-json-guard.ts +19 -0
  98. package/src/lib/nextauth-secret.ts +3 -1
  99. package/src/lib/no-store.ts +30 -0
  100. package/src/lib/payez-client-secret.ts +39 -0
  101. package/src/lib/qrcode.ts +348 -0
  102. package/src/lib/sign-in-failure.ts +88 -0
  103. package/src/lib/twofa-rejection.ts +7 -2
  104. package/src/middleware/create-middleware.ts +69 -9
  105. package/src/middleware/prefetch-guard-status.ts +56 -0
  106. package/src/pages/enroll/page.tsx +22 -0
  107. package/src/pages/login/page.tsx +2 -1
  108. package/src/routes/auth/session-reset.ts +46 -11
  109. package/src/routes/auth/session.ts +5 -1
  110. package/src/routes/auth/viability.ts +16 -4
  111. package/src/routes/health.ts +14 -0
  112. package/src/routes/mfa/methods.ts +3 -1
  113. package/src/routes/passkeys/list.ts +5 -1
  114. package/src/routes/session/refresh-viability.ts +5 -1
  115. package/src/utils/api.ts +50 -8
@@ -57,6 +57,7 @@ exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
57
57
  exports.storeIdpTokensInSession = storeIdpTokensInSession;
58
58
  exports.storePayezIdpSession = storePayezIdpSession;
59
59
  exports.expireSetCookies = expireSetCookies;
60
+ exports.authErrorCodeFromLocation = authErrorCodeFromLocation;
60
61
  exports.createAuthGetHandler = createAuthGetHandler;
61
62
  require("server-only");
62
63
  const node_async_hooks_1 = require("node:async_hooks");
@@ -77,6 +78,10 @@ const single_flight_1 = require("../lib/single-flight");
77
78
  const session_store_1 = require("../lib/session-store");
78
79
  const token_log_id_1 = require("../lib/token-log-id");
79
80
  const log_safe_1 = require("../lib/log-safe");
81
+ const better_auth_exposure_1 = require("./better-auth-exposure");
82
+ const no_store_1 = require("../lib/no-store");
83
+ /** PAY-2592: the magic-link advisory warning fires once per process. */
84
+ let magicLinkAdvisoryWarned = false;
80
85
  /**
81
86
  * 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
82
87
  * to tell "same" from "divergent", never enough to reconstruct. 12 hex chars
@@ -488,6 +493,12 @@ function buildBetterAuthProviders(config) {
488
493
  function createBetterAuthInstance(idpConfig, opts = {}) {
489
494
  (0, app_slug_1.assertAppSlugMatchesIdp)(idpConfig.clientSlug);
490
495
  const appSlug = idpConfig.clientSlug || (0, app_slug_1.getAppSlug)();
496
+ // PAY-2592 (GHSA-965c-763c-88jm): a consumer that enables the magic-link plugin on a better-auth below 1.7.7 is told, once per process. What keeps the
497
+ // package unexposed is account.storeStateStrategy='cookie' below plus the plugin being opt-in; see better-auth-exposure.ts.
498
+ if (opts.magicLink && !magicLinkAdvisoryWarned) {
499
+ magicLinkAdvisoryWarned = true;
500
+ (0, better_auth_exposure_1.warnIfMagicLinkBelowFix)(true);
501
+ }
491
502
  // 185121 line 1: name the secret this instance PINNED, as a hash, never the
492
503
  // secret. 183047's surviving fork is HMAC-fail vs record-miss; an HMAC-fail
493
504
  // caused by a divergent secret is only diagnosable if every instance
@@ -1376,6 +1387,20 @@ async function abortOAuthSignIn(callbackResponse, sessionToken, cookieHeader, lo
1376
1387
  console.warn('[BETTER_AUTH] OAuth sign-in ABORTED: the IdP exchange stored no tokens, so no session is kept (PAY-1733)', sessionToken ? (0, token_log_id_1.tokenLogId)(sessionToken) : '(no session)', 'code=' + errorCode);
1377
1388
  return new Response(null, { status: 302, headers });
1378
1389
  }
1390
+ /**
1391
+ * PAY-2568: the Better Auth error code of an /api/auth/error redirect (`?error=<code>`), or null when absent or not a plain
1392
+ * token. The value rides a URL, so only a short [A-Za-z0-9_.-] token is ever returned for logging. Location may be relative
1393
+ * or absolute; the base is a placeholder used only for parsing.
1394
+ */
1395
+ function authErrorCodeFromLocation(location) {
1396
+ try {
1397
+ const code = new URL(location, 'http://placeholder.invalid').searchParams.get('error');
1398
+ return code && /^[A-Za-z0-9_.-]{1,64}$/.test(code) ? code : null;
1399
+ }
1400
+ catch {
1401
+ return null;
1402
+ }
1403
+ }
1379
1404
  /**
1380
1405
  * Create a production-ready GET handler for the auth catch-all route.
1381
1406
  *
@@ -1395,7 +1420,7 @@ async function abortOAuthSignIn(callbackResponse, sessionToken, cookieHeader, lo
1395
1420
  * ```
1396
1421
  */
1397
1422
  function createAuthGetHandler(loginPath = '/account-auth/login') {
1398
- return async function GET(request) {
1423
+ const get = async function GET(request) {
1399
1424
  // Card 138635: establish the request-scoped carrier for the provider's raw
1400
1425
  // id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
1401
1426
  // hook that sees the token) and the exchange below (which forwards it).
@@ -1411,7 +1436,16 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1411
1436
  if (response.status === 302) {
1412
1437
  const location = response.headers.get('location') || '';
1413
1438
  if (location.includes('/api/auth/error') || location.includes('please_restart')) {
1414
- console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
1439
+ // PAY-2568: log WHICH error. Better Auth routes every callback failure to /api/auth/error?error=<code>
1440
+ // (signup_disabled, state_mismatch, ...); the old fixed 'OAuth state error' sentence made distinct faults
1441
+ // look identical. Only the code is logged - never the Location (it can carry other params).
1442
+ const authErrorCode = authErrorCodeFromLocation(location);
1443
+ if (authErrorCode) {
1444
+ console.warn('[BETTER_AUTH] auth error code=' + authErrorCode + ', redirecting to login');
1445
+ }
1446
+ else {
1447
+ console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
1448
+ }
1415
1449
  // *** DO NOT "TIDY" THIS BACK TO Response.redirect(). IT THROWS. (328439) ***
1416
1450
  // Response.redirect() REQUIRES an absolute URL — measured, on the leg's own
1417
1451
  // runtime and again on node 24:
@@ -1448,7 +1482,10 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1448
1482
  // Works whether loginPath is relative (package default '/account-auth/login',
1449
1483
  // and the graft passes '/sign-in') or absolute — an absolute Location is also
1450
1484
  // legal, so a consumer passing a full URL still behaves.
1451
- return new Response(null, { status: 302, headers: { location: loginPath } });
1485
+ // PAY-2554: the redirect used to carry NO code, so the login page showed nothing and looked like nothing happened.
1486
+ // It now carries the same neutral OAUTH_SIGN_IN_FAILED the abort path uses (the login page maps it to one sentence;
1487
+ // no provider text, URL or internal detail goes on the URL).
1488
+ return new Response(null, { status: 302, headers: { location: `${loginPath}${loginPath.includes('?') ? '&' : '?'}error=${exports.OAUTH_SIGN_IN_FAILED_ERROR}` } });
1452
1489
  }
1453
1490
  }
1454
1491
  // After the OAuth callback: exchange the provider identity for IDP tokens. If that does NOT store tokens - the
@@ -1496,4 +1533,6 @@ function createAuthGetHandler(loginPath = '/account-auth/login') {
1496
1533
  return response;
1497
1534
  });
1498
1535
  };
1536
+ // PAY-2994: every Better Auth GET answer (session, OAuth callbacks, redirects) is per-user and may set cookies; never cached.
1537
+ return (0, no_store_1.noStore)(get);
1499
1538
  }
@@ -75,6 +75,15 @@ const defaultUnauthenticatedRoutes = [
75
75
  allowDuringCircuitBreakerOpen: true,
76
76
  requiresRateLimit: false
77
77
  },
78
+ {
79
+ // PAY-2804: the exact path. '/api/health/*' matches startsWith('/api/health/'), which the bare
80
+ // '/api/health' (the ALB check) does not, so without this entry it sat behind sign-in.
81
+ pattern: '/api/health',
82
+ description: 'Health check endpoint (ALB target; anonymous, state only)',
83
+ allowDuringCircuitBreakerOpen: true,
84
+ // An ALB check comes from few IPs: a rate limit here would answer 429 and get tasks pulled (NightHawk 89179).
85
+ requiresRateLimit: false
86
+ },
78
87
  {
79
88
  pattern: '/api/health/*',
80
89
  description: 'Health check endpoints',
@@ -15,6 +15,26 @@ export interface ResetParams {
15
15
  }
16
16
  /** The secret the app already signs its session cookie with. Undefined means "cannot sign": callers fall back. */
17
17
  export declare function resetSecret(): string | undefined;
18
+ /**
19
+ * The domain-separation label for the cookie-derived FALLBACK key (PAY-2218, BAPert 83028). It is the only place the session cookie is used as
20
+ * key material, and it is never the raw SHA-256 of the cookie (that is the binding, a different value for a different purpose).
21
+ */
22
+ export declare const RESET_COOKIE_KEY_CONTEXT = "payez:session-reset:v1:cookie-derived";
23
+ /**
24
+ * The secret the reset link is signed and verified with.
25
+ *
26
+ * 1. The env secret (BETTER_AUTH_SECRET / NEXTAUTH_SECRET) when the edge can see it. ALWAYS FIRST: where it is present nothing changes.
27
+ * 2. Otherwise a key derived from the session cookie the link is bound to: HMAC(key = cookie value, message = RESET_COOKIE_KEY_CONTEXT).
28
+ * Why that is safe: the cookie is HttpOnly and is the user's credential, so a third-party page that links a victim to the route cannot
29
+ * produce a valid signature (the logout-CSRF protection the signature exists for), and anyone who holds the cookie can already act as
30
+ * the user, logging out included. The edge holds only a one-way value computed per request from a cookie it already received.
31
+ * RESIDUAL RISK, ACCEPTED (BAPert 83028): an attacker who controls a sibling subdomain can plant a cookie value they know under the
32
+ * session cookie name and sign a link that clears THAT value. The effect is a forced logout, and an attacker in that position can already
33
+ * set or shadow cookies and log the victim out, so the scheme adds no capability.
34
+ * 3. Neither: undefined ("cannot sign"; callers fall back to the direct clear).
35
+ * Never log the cookie value or the returned key.
36
+ */
37
+ export declare function resetSigningSecret(envSecret: string | undefined, sessionCookieValue: string | undefined): Promise<string | undefined>;
18
38
  /** The session cookie a reset is bound to: the __Secure- one when present, else the plain one. */
19
39
  export declare function boundSessionCookie(get: (name: string) => string | undefined, plainName: string, secureName: string): string | undefined;
20
40
  /** Normalise to exactly what the route will honour, so what is signed is what is verified. */
@@ -32,6 +52,18 @@ export declare function loginLocation(p: ResetParams): string;
32
52
  * `nowMs` is injectable for tests.
33
53
  */
34
54
  export declare function signedResetHref(route: string, p: ResetParams, secret: string, sessionCookieValue: string | undefined, nowMs?: number): Promise<string>;
55
+ /**
56
+ * VERIFY side, ONLY for an app that opted in (sessionResetCookieKey + createSessionResetHandler({ cookieKey: true }); BAPert 83102). PAY-2218, BAPert 83074 option a: the signer and the verifier can see different things. The edge middleware may be BLIND to the env
57
+ * secret (it signs cookie-derived) while the Node route handler is SIGHTED (the IdP-supplied secret is in its process.env). So the route accepts a link
58
+ * signed with EITHER sound key: the env secret first (when it has one), then the key derived from THIS request's session cookie. Both are sound: the
59
+ * cookie-derived link is still bound to the request's own session cookie, carries its expiry inside the MAC, and is compared in constant time. A link
60
+ * signed with neither key, or with a key derived from a different cookie, is refused.
61
+ */
62
+ export declare function readResetRequestAnyKey(search: URLSearchParams, envSecret: string | undefined, sessionCookieValue: string | undefined, nowMs?: number): Promise<{
63
+ params: ResetParams;
64
+ signed: boolean;
65
+ via: 'env' | 'cookie' | null;
66
+ }>;
35
67
  /**
36
68
  * Read a reset request's query. `signed` is true only for a valid, unexpired signature over the normalised params AND
37
69
  * this request's own session cookie (`sessionCookieValue`). With no session cookie there is nothing to clear: false.
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RESET_ERRORS = exports.RESET_REASONS = exports.RESET_KEY_PURPOSE = exports.RESET_LINK_TTL_SECONDS = exports.DEFAULT_SESSION_RESET_ROUTE = exports.LOGIN_PAGE = void 0;
3
+ exports.RESET_COOKIE_KEY_CONTEXT = exports.RESET_ERRORS = exports.RESET_REASONS = exports.RESET_KEY_PURPOSE = exports.RESET_LINK_TTL_SECONDS = exports.DEFAULT_SESSION_RESET_ROUTE = exports.LOGIN_PAGE = void 0;
4
4
  exports.resetSecret = resetSecret;
5
+ exports.resetSigningSecret = resetSigningSecret;
5
6
  exports.boundSessionCookie = boundSessionCookie;
6
7
  exports.normaliseResetParams = normaliseResetParams;
7
8
  exports.resetPayload = resetPayload;
8
9
  exports.loginLocation = loginLocation;
9
10
  exports.signedResetHref = signedResetHref;
11
+ exports.readResetRequestAnyKey = readResetRequestAnyKey;
10
12
  exports.readResetRequest = readResetRequest;
11
13
  /**
12
14
  * PAY-2218 PR 2: the hand-off between the middleware and the session-reset route.
@@ -85,6 +87,32 @@ function sameString(a, b) {
85
87
  function resetSecret() {
86
88
  return process.env.BETTER_AUTH_SECRET || process.env.NEXTAUTH_SECRET || undefined;
87
89
  }
90
+ /**
91
+ * The domain-separation label for the cookie-derived FALLBACK key (PAY-2218, BAPert 83028). It is the only place the session cookie is used as
92
+ * key material, and it is never the raw SHA-256 of the cookie (that is the binding, a different value for a different purpose).
93
+ */
94
+ exports.RESET_COOKIE_KEY_CONTEXT = 'payez:session-reset:v1:cookie-derived';
95
+ /**
96
+ * The secret the reset link is signed and verified with.
97
+ *
98
+ * 1. The env secret (BETTER_AUTH_SECRET / NEXTAUTH_SECRET) when the edge can see it. ALWAYS FIRST: where it is present nothing changes.
99
+ * 2. Otherwise a key derived from the session cookie the link is bound to: HMAC(key = cookie value, message = RESET_COOKIE_KEY_CONTEXT).
100
+ * Why that is safe: the cookie is HttpOnly and is the user's credential, so a third-party page that links a victim to the route cannot
101
+ * produce a valid signature (the logout-CSRF protection the signature exists for), and anyone who holds the cookie can already act as
102
+ * the user, logging out included. The edge holds only a one-way value computed per request from a cookie it already received.
103
+ * RESIDUAL RISK, ACCEPTED (BAPert 83028): an attacker who controls a sibling subdomain can plant a cookie value they know under the
104
+ * session cookie name and sign a link that clears THAT value. The effect is a forced logout, and an attacker in that position can already
105
+ * set or shadow cookies and log the victim out, so the scheme adds no capability.
106
+ * 3. Neither: undefined ("cannot sign"; callers fall back to the direct clear).
107
+ * Never log the cookie value or the returned key.
108
+ */
109
+ async function resetSigningSecret(envSecret, sessionCookieValue) {
110
+ if (envSecret)
111
+ return envSecret;
112
+ if (!sessionCookieValue)
113
+ return undefined;
114
+ return b64url(await hmacBytes(encoder.encode(sessionCookieValue), exports.RESET_COOKIE_KEY_CONTEXT));
115
+ }
88
116
  /** The session cookie a reset is bound to: the __Secure- one when present, else the plain one. */
89
117
  function boundSessionCookie(get, plainName, secureName) {
90
118
  return get(secureName) || get(plainName) || undefined;
@@ -126,6 +154,22 @@ async function signedResetHref(route, p, secret, sessionCookieValue, nowMs = Dat
126
154
  q.set('sig', await sign(secret, resetPayload(p, exp, await sessionBinding(sessionCookieValue))));
127
155
  return `${route}?${q.toString()}`;
128
156
  }
157
+ /**
158
+ * VERIFY side, ONLY for an app that opted in (sessionResetCookieKey + createSessionResetHandler({ cookieKey: true }); BAPert 83102). PAY-2218, BAPert 83074 option a: the signer and the verifier can see different things. The edge middleware may be BLIND to the env
159
+ * secret (it signs cookie-derived) while the Node route handler is SIGHTED (the IdP-supplied secret is in its process.env). So the route accepts a link
160
+ * signed with EITHER sound key: the env secret first (when it has one), then the key derived from THIS request's session cookie. Both are sound: the
161
+ * cookie-derived link is still bound to the request's own session cookie, carries its expiry inside the MAC, and is compared in constant time. A link
162
+ * signed with neither key, or with a key derived from a different cookie, is refused.
163
+ */
164
+ async function readResetRequestAnyKey(search, envSecret, sessionCookieValue, nowMs = Date.now()) {
165
+ if (envSecret) {
166
+ const viaEnv = await readResetRequest(search, envSecret, sessionCookieValue, nowMs);
167
+ if (viaEnv.signed)
168
+ return { ...viaEnv, via: 'env' };
169
+ }
170
+ const viaCookie = await readResetRequest(search, await resetSigningSecret(undefined, sessionCookieValue), sessionCookieValue, nowMs);
171
+ return { ...viaCookie, via: viaCookie.signed ? 'cookie' : null };
172
+ }
129
173
  /**
130
174
  * Read a reset request's query. `signed` is true only for a valid, unexpired signature over the normalised params AND
131
175
  * this request's own session cookie (`sessionCookieValue`). With no session cookie there is nothing to clear: false.
@@ -27,7 +27,13 @@ interface Props {
27
27
  /** Test seam: navigation after success. */
28
28
  onSignedIn?: (destination: string) => void;
29
29
  showForgotPassword?: boolean;
30
+ /**
31
+ * PAY-2630: the enrolment page a user the IdP told to enrol is taken to after signing in. NO DEFAULT: the offer renders only when
32
+ * the host passes this, because the host is the one that serves the page (the CLI scaffold writes no /account/security, so a
33
+ * default would 404 a generated app's user right after a good sign-in).
34
+ */
35
+ enrollHref?: string;
30
36
  }
31
37
  export declare function resolveDestination(explicit?: string): string;
32
- export declare function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPassword }: Props): import("react/jsx-runtime").JSX.Element;
38
+ export declare function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPassword, enrollHref }: Props): import("react/jsx-runtime").JSX.Element;
33
39
  export default CredentialSignInForm;
@@ -10,6 +10,7 @@ const safe_callback_1 = require("../../auth/safe-callback");
10
10
  const authorize_resume_1 = require("../../lib/authorize-resume");
11
11
  const code_step_copy_1 = require("../../lib/code-step-copy");
12
12
  const DevCodeNotice_1 = require("./DevCodeNotice");
13
+ const EnrollAuthenticatorOffer_1 = require("./EnrollAuthenticatorOffer");
13
14
  const credential_sign_in_client_1 = require("../../lib/credential-sign-in-client");
14
15
  const inputStyle = {
15
16
  borderColor: 'var(--border-default, #e5e7eb)',
@@ -30,26 +31,31 @@ function resolveDestination(explicit) {
30
31
  }
31
32
  return (0, safe_callback_1.safeCallbackPath)((0, authorize_resume_1.authorizeResumeHref)(returnUrl), (0, safe_callback_1.safeCallbackPath)(raw, '/dashboard'));
32
33
  }
33
- function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPassword = false }) {
34
+ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPassword = false, enrollHref }) {
34
35
  const [step, setStep] = (0, react_1.useState)('credentials');
35
36
  const [email, setEmail] = (0, react_1.useState)('');
36
37
  const [password, setPassword] = (0, react_1.useState)('');
37
38
  const [code, setCode] = (0, react_1.useState)('');
38
39
  const [busy, setBusy] = (0, react_1.useState)(false);
39
40
  const [error, setError] = (0, react_1.useState)(null);
41
+ const [errorCode, setErrorCode] = (0, react_1.useState)(null);
40
42
  const [fieldError, setFieldError] = (0, react_1.useState)(null);
41
43
  const [codeRequest, setCodeRequest] = (0, react_1.useState)(code_step_copy_1.NO_CODE_REQUEST);
42
44
  const [offerEmailCode, setOfferEmailCode] = (0, react_1.useState)(false);
43
45
  const [devCode, setDevCode] = (0, react_1.useState)(null);
44
46
  const [emailCodeMsg, setEmailCodeMsg] = (0, react_1.useState)(null);
47
+ // PAY-2630: what the IdP said is outstanding on this client, and whether the user kept the 'take me to enrol' offer ticked.
48
+ const [nextStep, setNextStep] = (0, react_1.useState)(null);
49
+ const [enrolAfter, setEnrolAfter] = (0, react_1.useState)(true);
45
50
  async function submit(withCode) {
46
51
  setBusy(true);
47
52
  setError(null);
53
+ setErrorCode(null);
48
54
  setOfferEmailCode(false);
49
55
  try {
50
56
  const r = await (0, credential_sign_in_client_1.credentialSignIn)({ email, password, code: withCode }, fetchImpl);
51
57
  if (r.kind === 'success') {
52
- const destination = resolveDestination(callbackUrl);
58
+ const destination = (0, credential_sign_in_client_1.postSignInDestination)(resolveDestination(callbackUrl), nextStep, enrolAfter, enrollHref);
53
59
  if (onSignedIn)
54
60
  onSignedIn(destination);
55
61
  else
@@ -59,10 +65,12 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
59
65
  if (r.kind === 'two_factor') {
60
66
  setCodeRequest(r.codeRequest);
61
67
  setDevCode(r.devCode);
68
+ setNextStep(r.nextStep);
62
69
  setStep('code');
63
70
  return;
64
71
  }
65
72
  setError(r.message);
73
+ setErrorCode(r.code);
66
74
  if (r.kind === 'error')
67
75
  setOfferEmailCode(r.offerEmailCode);
68
76
  }
@@ -94,6 +102,7 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
94
102
  setStep('credentials');
95
103
  setCodeRequest(code_step_copy_1.NO_CODE_REQUEST);
96
104
  setDevCode(null);
105
+ setNextStep(null);
97
106
  setCode('');
98
107
  setFieldError(null);
99
108
  setError(null);
@@ -121,12 +130,12 @@ function CredentialSignInForm({ callbackUrl, fetchImpl, onSignedIn, showForgotPa
121
130
  }
122
131
  }
123
132
  const problem = fieldError ?? error;
124
- const problemLine = problem && ((0, jsx_runtime_1.jsx)("p", { role: "alert", "data-testid": "credential-sign-in-error", className: "text-sm mt-3", style: { color: 'var(--color-error, #ef4444)' }, children: problem }));
133
+ const problemLine = problem && ((0, jsx_runtime_1.jsx)("p", { role: "alert", "data-testid": "credential-sign-in-error", "data-error-code": fieldError ? 'invalid_input' : (errorCode ?? undefined), className: "text-sm mt-3", style: { color: 'var(--color-error, #ef4444)' }, children: problem }));
125
134
  if (step === 'code') {
126
135
  return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "credential-sign-in-code-step", className: "text-left", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm mb-4", style: { color: 'var(--text-primary, #111827)' }, children: (0, code_step_copy_1.codeStepInstruction)(email, codeRequest) }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: onCodeSubmit, noValidate: true, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "credential-code", className: "block text-sm font-medium mb-2", style: { color: 'var(--text-primary, #111827)' }, children: "Authentication code" }), (0, jsx_runtime_1.jsx)("input", { id: "credential-code", name: "code", type: "text", inputMode: "numeric", autoComplete: "one-time-code", required: true, placeholder: "000000", value: code, disabled: busy, onChange: (e) => {
127
136
  setCode(e.target.value);
128
137
  setFieldError(null);
129
- }, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: inputStyle }), (0, jsx_runtime_1.jsx)(DevCodeNotice_1.DevCodeNotice, { code: devCode })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy, className: "w-full py-3 px-4 rounded-lg font-semibold focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", style: { background: 'var(--btn-primary-bg, #11B588)', color: 'var(--btn-primary-text, white)' }, children: busy ? 'Verifying…' : 'Verify' })] }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onBack, disabled: busy, className: "text-sm underline mt-4", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-muted, #6b7280)' }, children: "Back" }), problemLine, (offerEmailCode || emailCodeMsg) && ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "email-code-offer", className: "mt-4", children: [offerEmailCode && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => void askForEmailCode(), disabled: busy, className: "text-sm font-semibold underline", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-primary, #111827)' }, children: "Email me a code instead" })), emailCodeMsg && ((0, jsx_runtime_1.jsx)("p", { role: "status", "data-testid": "email-code-offer-msg", className: "text-sm mt-2", style: { color: 'var(--text-primary, #111827)' }, children: emailCodeMsg }))] }))] }));
138
+ }, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: inputStyle }), (0, jsx_runtime_1.jsx)(DevCodeNotice_1.DevCodeNotice, { code: devCode })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: busy, className: "w-full py-3 px-4 rounded-lg font-semibold focus:outline-none focus:ring-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors", style: { background: 'var(--btn-primary-bg, #11B588)', color: 'var(--btn-primary-text, white)' }, children: busy ? 'Verifying…' : 'Verify' })] }), (0, jsx_runtime_1.jsx)(EnrollAuthenticatorOffer_1.EnrollAuthenticatorOffer, { nextStep: enrollHref ? nextStep : null, checked: enrolAfter, onChange: setEnrolAfter, disabled: busy }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onBack, disabled: busy, className: "text-sm underline mt-4", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-muted, #6b7280)' }, children: "Back" }), problemLine, (offerEmailCode || emailCodeMsg) && ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "email-code-offer", className: "mt-4", children: [offerEmailCode && ((0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => void askForEmailCode(), disabled: busy, className: "text-sm font-semibold underline", style: { background: 'none', border: 'none', padding: 0, color: 'var(--text-primary, #111827)' }, children: "Email me a code instead" })), emailCodeMsg && ((0, jsx_runtime_1.jsx)("p", { role: "status", "data-testid": "email-code-offer-msg", className: "text-sm mt-2", style: { color: 'var(--text-primary, #111827)' }, children: emailCodeMsg }))] }))] }));
130
139
  }
131
140
  return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "credential-sign-in", className: "text-left", children: [(0, jsx_runtime_1.jsx)(TraditionalAuthSection_1.TraditionalAuthSection, { email: email, password: password, onEmailChange: (v) => {
132
141
  setEmail(v);
@@ -0,0 +1,21 @@
1
+ import type { NextStep } from '../../lib/credential-sign-in-client';
2
+ /**
3
+ * PAY-2630: the code step's offer to set up an authenticator.
4
+ *
5
+ * WHY. The IdP answers the first sign-in turn with next_step 'enroll_mfa' when the user has NO usable second factor on this
6
+ * client (factors are per client). The step used to ignore that and show only "enter the code we emailed you", so a user of a
7
+ * generated app could never enrol a factor on that client. Measured on the PAY-2305 walk (client 90662, user with a TOTP on
8
+ * client 12 only): requiresTwoFactor true, sentVia 'email', nextStep 'enroll_mfa', and a page with Verify / Back and nothing else.
9
+ *
10
+ * WHAT IT IS, AND IS NOT. It renders ONLY for 'enroll_mfa'. It names the lane the user is on (the emailed code) and offers to take
11
+ * them to the enrolment page right after they sign in with it. It does NOT enrol in place: the enrolment endpoints need a signed-in
12
+ * session and this step has none yet (the provisional bearer is parked server-side). For 'mfa_verify', null or anything else it
13
+ * renders nothing: a user who HAS a factor must not be told to set one up.
14
+ */
15
+ export declare function EnrollAuthenticatorOffer({ nextStep, checked, onChange, disabled, }: {
16
+ nextStep: NextStep;
17
+ checked: boolean;
18
+ onChange: (next: boolean) => void;
19
+ disabled?: boolean;
20
+ }): import("react/jsx-runtime").JSX.Element | null;
21
+ export default EnrollAuthenticatorOffer;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EnrollAuthenticatorOffer = EnrollAuthenticatorOffer;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ /**
6
+ * PAY-2630: the code step's offer to set up an authenticator.
7
+ *
8
+ * WHY. The IdP answers the first sign-in turn with next_step 'enroll_mfa' when the user has NO usable second factor on this
9
+ * client (factors are per client). The step used to ignore that and show only "enter the code we emailed you", so a user of a
10
+ * generated app could never enrol a factor on that client. Measured on the PAY-2305 walk (client 90662, user with a TOTP on
11
+ * client 12 only): requiresTwoFactor true, sentVia 'email', nextStep 'enroll_mfa', and a page with Verify / Back and nothing else.
12
+ *
13
+ * WHAT IT IS, AND IS NOT. It renders ONLY for 'enroll_mfa'. It names the lane the user is on (the emailed code) and offers to take
14
+ * them to the enrolment page right after they sign in with it. It does NOT enrol in place: the enrolment endpoints need a signed-in
15
+ * session and this step has none yet (the provisional bearer is parked server-side). For 'mfa_verify', null or anything else it
16
+ * renders nothing: a user who HAS a factor must not be told to set one up.
17
+ */
18
+ function EnrollAuthenticatorOffer({ nextStep, checked, onChange, disabled, }) {
19
+ if (nextStep !== 'enroll_mfa')
20
+ return null;
21
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "enroll-authenticator-offer", className: "mt-4 text-sm", style: { color: 'var(--text-primary, #111827)' }, children: [(0, jsx_runtime_1.jsx)("p", { "data-testid": "enroll-authenticator-lane", children: "You have no authenticator set up for this app yet, so this time you sign in with the emailed code." }), (0, jsx_runtime_1.jsxs)("label", { className: "flex items-center gap-2 mt-2", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", "data-testid": "enroll-authenticator-checkbox", checked: checked, disabled: disabled, onChange: (e) => onChange(e.target.checked) }), (0, jsx_runtime_1.jsx)("span", { children: "Take me to set up an authenticator right after I sign in" })] })] }));
22
+ }
23
+ exports.default = EnrollAuthenticatorOffer;
@@ -0,0 +1,6 @@
1
+ /** PAY-2631: client-side QR from an otpauth:// URI. Inline SVG, quiet zone of 4 modules; no network, no library. */
2
+ export declare function QrCode({ value, size }: {
3
+ value: string;
4
+ size?: number;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ export default QrCode;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QrCode = QrCode;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("react");
6
+ const qrcode_1 = require("../../lib/qrcode");
7
+ /** PAY-2631: client-side QR from an otpauth:// URI. Inline SVG, quiet zone of 4 modules; no network, no library. */
8
+ function QrCode({ value, size = 168 }) {
9
+ const qr = (0, react_1.useMemo)(() => (0, qrcode_1.encodeQr)(value), [value]);
10
+ const path = (0, react_1.useMemo)(() => (0, qrcode_1.qrToSvgPath)(qr), [qr]);
11
+ const modules = qr.size;
12
+ return ((0, jsx_runtime_1.jsxs)("svg", { width: size, height: size, viewBox: `-4 -4 ${modules + 8} ${modules + 8}`, role: "img", "aria-label": "QR code for authenticator setup", "data-testid": "totp-qr", children: [(0, jsx_runtime_1.jsx)("rect", { x: -4, y: -4, width: modules + 8, height: modules + 8, fill: "#ffffff" }), (0, jsx_runtime_1.jsx)("path", { d: path, fill: "#212126" })] }));
13
+ }
14
+ exports.default = QrCode;
@@ -0,0 +1,6 @@
1
+ export declare const STEP_UP_SENTENCE = "This session was signed in with an emailed or texted code only. Sign in with your current authenticator, then try again.";
2
+ export declare function TotpEnrollPanel({ fetchImpl, onConfirmed }?: {
3
+ fetchImpl?: typeof fetch;
4
+ onConfirmed?: () => void;
5
+ }): import("react/jsx-runtime").JSX.Element;
6
+ export default TotpEnrollPanel;
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ 'use client';
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.STEP_UP_SENTENCE = void 0;
5
+ exports.TotpEnrollPanel = TotpEnrollPanel;
6
+ const jsx_runtime_1 = require("react/jsx-runtime");
7
+ const react_1 = require("react");
8
+ const QrCode_1 = require("./QrCode");
9
+ const two_factor_verify_failure_1 = require("../../lib/two-factor-verify-failure");
10
+ exports.STEP_UP_SENTENCE = 'This session was signed in with an emailed or texted code only. Sign in with your current authenticator, then try again.';
11
+ async function readJson(res) {
12
+ return res.json().catch(() => null);
13
+ }
14
+ function TotpEnrollPanel({ fetchImpl, onConfirmed } = {}) {
15
+ const doFetch = fetchImpl ?? ((...a) => fetch(...a));
16
+ const [stage, setStage] = (0, react_1.useState)('idle');
17
+ const [enroll, setEnroll] = (0, react_1.useState)(null);
18
+ const [code, setCode] = (0, react_1.useState)('');
19
+ const [error, setError] = (0, react_1.useState)(null);
20
+ const [busy, setBusy] = (0, react_1.useState)(false);
21
+ async function begin() {
22
+ setBusy(true);
23
+ setError(null);
24
+ try {
25
+ const res = await doFetch('/api/mfa/totp/enroll', {
26
+ method: 'POST',
27
+ headers: { 'Content-Type': 'application/json' },
28
+ body: JSON.stringify({ friendly_name: 'This device' }),
29
+ });
30
+ if (res.ok) {
31
+ const body = (await readJson(res));
32
+ if (!body || !body.secret || !body.otpauth_uri || body.enrollment_id === undefined) {
33
+ setError('Something went wrong on our end. Try again.');
34
+ return;
35
+ }
36
+ setEnroll({ secret: body.secret, otpauth_uri: body.otpauth_uri, enrollment_id: body.enrollment_id });
37
+ setCode('');
38
+ setStage('enrolling');
39
+ return;
40
+ }
41
+ if (res.status === 401) {
42
+ setStage('signed-out');
43
+ return;
44
+ }
45
+ const json = await readJson(res);
46
+ if ((0, two_factor_verify_failure_1.stepUpRefusal)(res.status, json)) {
47
+ setError(exports.STEP_UP_SENTENCE);
48
+ return;
49
+ }
50
+ if (res.status === 403) {
51
+ setError('This account already has a second factor set up, so an authenticator app cannot be added here. Sign in with your existing method.');
52
+ return;
53
+ }
54
+ setError('Something went wrong on our end. Try again.');
55
+ }
56
+ finally {
57
+ setBusy(false);
58
+ }
59
+ }
60
+ async function confirm() {
61
+ if (!enroll || !/^\d{6}$/.test(code)) {
62
+ setError('Enter the 6-digit code from your app.');
63
+ return;
64
+ }
65
+ setBusy(true);
66
+ setError(null);
67
+ try {
68
+ const res = await doFetch('/api/mfa/totp/confirm', {
69
+ method: 'POST',
70
+ headers: { 'Content-Type': 'application/json' },
71
+ body: JSON.stringify({ enrollment_id: enroll.enrollment_id, code }),
72
+ });
73
+ if (res.ok) {
74
+ setEnroll(null);
75
+ setCode('');
76
+ setStage('done');
77
+ onConfirmed?.();
78
+ return;
79
+ }
80
+ if (res.status === 401) {
81
+ setStage('signed-out');
82
+ return;
83
+ }
84
+ const json = await readJson(res);
85
+ if ((0, two_factor_verify_failure_1.stepUpRefusal)(res.status, json)) {
86
+ setError(exports.STEP_UP_SENTENCE);
87
+ return;
88
+ }
89
+ setCode('');
90
+ setError(res.status === 400 || res.status === 422
91
+ ? 'That code did not match. Check the time on your phone and try the next code.'
92
+ : 'Setup was not completed. Try again.');
93
+ }
94
+ finally {
95
+ setBusy(false);
96
+ }
97
+ }
98
+ if (stage === 'signed-out') {
99
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "totp-signed-out", children: [(0, jsx_runtime_1.jsx)("a", { href: "/account-auth/login", children: "Sign in" }), " to set up an authenticator app."] }));
100
+ }
101
+ if (stage === 'done') {
102
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "totp-done", role: "status", children: [(0, jsx_runtime_1.jsx)("p", { children: "Authenticator app added." }), (0, jsx_runtime_1.jsx)("p", { children: "The next time you sign in on this app, you will be asked for the 6-digit code from it." })] }));
103
+ }
104
+ if (stage === 'enrolling' && enroll) {
105
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "totp-enrolling", style: { display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }, children: [(0, jsx_runtime_1.jsx)("p", { children: "Open your authenticator app and scan this code. A phone camera works too." }), (0, jsx_runtime_1.jsx)(QrCode_1.QrCode, { value: enroll.otpauth_uri }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { children: "Can't scan? Enter this code manually:" }), (0, jsx_runtime_1.jsx)("code", { "data-testid": "totp-secret", style: { wordBreak: 'break-all' }, children: enroll.secret })] }), (0, jsx_runtime_1.jsxs)("form", { onSubmit: (e) => {
106
+ e.preventDefault();
107
+ void confirm();
108
+ }, children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "totp-code", children: "Enter the 6-digit code from your app:" }), (0, jsx_runtime_1.jsx)("br", {}), (0, jsx_runtime_1.jsx)("input", { id: "totp-code", "data-testid": "totp-code", inputMode: "numeric", autoComplete: "one-time-code", maxLength: 6, value: code, disabled: busy, onChange: (e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6)) }), ' ', (0, jsx_runtime_1.jsx)("button", { type: "submit", "data-testid": "totp-confirm", disabled: busy || code.length !== 6, children: "Confirm" })] }), error ? ((0, jsx_runtime_1.jsx)("p", { role: "alert", "data-testid": "totp-error", children: error })) : null, (0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "totp-restart", disabled: busy, onClick: () => {
109
+ setEnroll(null);
110
+ setCode('');
111
+ setError(null);
112
+ setStage('idle');
113
+ }, children: "Start again" })] }));
114
+ }
115
+ return ((0, jsx_runtime_1.jsxs)("div", { "data-testid": "totp-idle", children: [(0, jsx_runtime_1.jsx)("button", { type: "button", "data-testid": "totp-begin", disabled: busy, onClick: () => void begin(), children: busy ? 'Starting…' : 'Add authenticator app' }), error ? ((0, jsx_runtime_1.jsx)("p", { role: "alert", "data-testid": "totp-error", children: error })) : null] }));
116
+ }
117
+ exports.default = TotpEnrollPanel;
@@ -7,7 +7,7 @@
7
7
  export { makeAuthDecision } from '../auth/auth-decision';
8
8
  export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig, configure2FABypassRoutes, should2FABypass, get2FABypassConfig } from '../auth/route-config';
9
9
  export type { UnauthenticatedRouteConfig, ProtectedRouteConfig } from '../auth/route-config';
10
- export { createMvpMiddleware, isPrefetchRequest } from '../middleware/create-middleware';
10
+ export { createMvpMiddleware, isPrefetchRequest, isKnownNonNavigationRequest } from '../middleware/create-middleware';
11
11
  export type { MvpMiddlewareOptions, CircuitBreakerProvider, MiddlewareLogger, AuthContext, AuthAction, SessionPointer, SessionStatus } from '../middleware/create-middleware';
12
12
  export { TwoFactorPresets, AMRValues, ACRLevels, validateAMR, validateACR, checkTwoFactorRequirements } from '../middleware/twofa-presets';
13
13
  export type { TwoFactorRequirements, RouteConfig } from '../middleware/twofa-presets';
@@ -6,7 +6,7 @@
6
6
  * Client-side utilities that depend on browser APIs or server-only code are excluded.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.getInternalApiUrl = exports.checkTwoFactorRequirements = exports.validateACR = exports.validateAMR = exports.ACRLevels = exports.AMRValues = exports.TwoFactorPresets = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.get2FABypassConfig = exports.should2FABypass = exports.configure2FABypassRoutes = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = void 0;
9
+ exports.getInternalApiUrl = exports.checkTwoFactorRequirements = exports.validateACR = exports.validateAMR = exports.ACRLevels = exports.AMRValues = exports.TwoFactorPresets = exports.isKnownNonNavigationRequest = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.get2FABypassConfig = exports.should2FABypass = exports.configure2FABypassRoutes = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = void 0;
10
10
  // Authentication middleware and configuration (Edge Runtime compatible)
11
11
  var auth_decision_1 = require("../auth/auth-decision");
12
12
  Object.defineProperty(exports, "makeAuthDecision", { enumerable: true, get: function () { return auth_decision_1.makeAuthDecision; } });
@@ -20,6 +20,7 @@ Object.defineProperty(exports, "get2FABypassConfig", { enumerable: true, get: fu
20
20
  var create_middleware_1 = require("../middleware/create-middleware");
21
21
  Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
22
22
  Object.defineProperty(exports, "isPrefetchRequest", { enumerable: true, get: function () { return create_middleware_1.isPrefetchRequest; } });
23
+ Object.defineProperty(exports, "isKnownNonNavigationRequest", { enumerable: true, get: function () { return create_middleware_1.isKnownNonNavigationRequest; } });
23
24
  // Two-Factor Authentication Presets (Edge Runtime compatible)
24
25
  var twofa_presets_1 = require("../middleware/twofa-presets");
25
26
  Object.defineProperty(exports, "TwoFactorPresets", { enumerable: true, get: function () { return twofa_presets_1.TwoFactorPresets; } });
package/dist/index.d.ts CHANGED
@@ -6,7 +6,7 @@ export type { AuthConfig } from './types/auth';
6
6
  export { makeAuthDecision } from './auth/auth-decision';
7
7
  export { isUnauthenticatedRoute, configurePublicRoutes, getRouteConfig } from './auth/route-config';
8
8
  export { safeCallbackPath, isSafeCallbackPath, resolveCallbackDestination, DEFAULT_CALLBACK } from './auth/safe-callback';
9
- export { createMvpMiddleware, isPrefetchRequest } from './middleware/create-middleware';
9
+ export { createMvpMiddleware, isPrefetchRequest, isKnownNonNavigationRequest } from './middleware/create-middleware';
10
10
  export { UserAvatarMenu, MobileNavDrawer } from './components/account';
11
11
  export type { UserAvatarMenuProps, MobileNavDrawerProps, NavItem, NavSection } from './components/account';
12
12
  export { ErrorMetricsCard, HealthMetricsCard, AuditLogViewer, AdminAnalyticsLayout, useErrorMetrics, useHealthMetrics, useAuditLog, useAdminAnalytics, getErrorMetrics, getHealthMetrics, writeAuditLog, queryAuditLog, } from './logging';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // - Better Auth: import { createBetterAuthInstance } from '@payez/next-mvp/auth/better-auth'
9
9
  // - Server auth: import { getSession } from '@payez/next-mvp/server/auth'
10
10
  Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.DEFAULT_CALLBACK = exports.resolveCallbackDestination = exports.isSafeCallbackPath = exports.safeCallbackPath = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
11
+ exports.queryAuditLog = exports.writeAuditLog = exports.getHealthMetrics = exports.getErrorMetrics = exports.useAdminAnalytics = exports.useAuditLog = exports.useHealthMetrics = exports.useErrorMetrics = exports.AdminAnalyticsLayout = exports.AuditLogViewer = exports.HealthMetricsCard = exports.ErrorMetricsCard = exports.MobileNavDrawer = exports.UserAvatarMenu = exports.isKnownNonNavigationRequest = exports.isPrefetchRequest = exports.createMvpMiddleware = exports.DEFAULT_CALLBACK = exports.resolveCallbackDestination = exports.isSafeCallbackPath = exports.safeCallbackPath = exports.getRouteConfig = exports.configurePublicRoutes = exports.isUnauthenticatedRoute = exports.makeAuthDecision = exports.useTraditionalAuthEnabled = exports.useFederatedAuthEnabled = exports.useFederatedProviders = exports.useAuthMode = exports.useAuthConfig = exports.AuthProvider = exports.useAnonSession = exports.fetchWithAuth = void 0;
12
12
  // Client-safe exports only
13
13
  // Client-side utilities
14
14
  var fetch_with_auth_1 = require("./client/fetch-with-auth");
@@ -41,6 +41,7 @@ Object.defineProperty(exports, "DEFAULT_CALLBACK", { enumerable: true, get: func
41
41
  var create_middleware_1 = require("./middleware/create-middleware");
42
42
  Object.defineProperty(exports, "createMvpMiddleware", { enumerable: true, get: function () { return create_middleware_1.createMvpMiddleware; } });
43
43
  Object.defineProperty(exports, "isPrefetchRequest", { enumerable: true, get: function () { return create_middleware_1.isPrefetchRequest; } });
44
+ Object.defineProperty(exports, "isKnownNonNavigationRequest", { enumerable: true, get: function () { return create_middleware_1.isKnownNonNavigationRequest; } });
44
45
  // Account Components
45
46
  var account_1 = require("./components/account");
46
47
  Object.defineProperty(exports, "UserAvatarMenu", { enumerable: true, get: function () { return account_1.UserAvatarMenu; } });