@payez/next-mvp 4.4.1 → 4.4.3

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 (39) hide show
  1. package/dist/api/auth-handler.js +13 -4
  2. package/dist/api-handlers/mfa/totp-verify.d.ts +3 -1
  3. package/dist/api-handlers/mfa/totp-verify.js +22 -3
  4. package/dist/api-handlers/session/refresh-viability.js +2 -2
  5. package/dist/api-handlers/session/viability.js +1 -1
  6. package/dist/auth/auth-decision.js +27 -8
  7. package/dist/auth/better-auth.js +123 -1
  8. package/dist/auth/safe-callback.d.ts +92 -0
  9. package/dist/auth/safe-callback.js +183 -0
  10. package/dist/hooks/useProfile.d.ts +0 -1
  11. package/dist/lib/acr.d.ts +64 -0
  12. package/dist/lib/acr.js +109 -0
  13. package/dist/lib/session-store.js +56 -7
  14. package/dist/middleware/create-middleware.js +45 -13
  15. package/dist/pages/profile/EnhancedProfilePage.js +38 -13
  16. package/dist/pages/verify-code/page.js +26 -1
  17. package/dist/routes/auth/viability.d.ts +2 -2
  18. package/dist/routes/auth/viability.js +2 -2
  19. package/dist/server/auth-guard.js +26 -1
  20. package/package.json +2 -1
  21. package/scripts/module-reach.mjs +280 -0
  22. package/src/api/auth-handler.ts +19 -4
  23. package/src/api-handlers/mfa/totp-verify.ts +24 -3
  24. package/src/api-handlers/session/refresh-viability.ts +2 -2
  25. package/src/api-handlers/session/viability.ts +1 -1
  26. package/src/auth/auth-decision.ts +28 -8
  27. package/src/auth/better-auth.ts +128 -1
  28. package/src/auth/safe-callback.ts +185 -0
  29. package/src/hooks/useProfile.ts +8 -1
  30. package/src/lib/acr.ts +109 -0
  31. package/src/lib/session-store.ts +66 -7
  32. package/src/middleware/create-middleware.ts +45 -13
  33. package/src/pages/profile/EnhancedProfilePage.tsx +43 -14
  34. package/src/pages/verify-code/page.tsx +26 -1
  35. package/src/routes/auth/viability.ts +2 -2
  36. package/src/server/auth-guard.ts +27 -1
  37. package/dist/middleware/auth-decision.d.ts +0 -33
  38. package/dist/middleware/auth-decision.js +0 -65
  39. package/src/middleware/auth-decision.ts +0 -43
@@ -127,7 +127,33 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
127
127
  }
128
128
 
129
129
  if (requires2FA) {
130
- const mfaVerified = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
130
+ // 369858: the legacy `?? (sessionData as any).twoFactorComplete` carry is REMOVED.
131
+ // This is THE access gate - the only one of the 8 carry sites that grants access
132
+ // rather than reporting status - so it reads exactly one source of truth.
133
+ //
134
+ // Removing it is fail-closed. No current writer emits the legacy field alone:
135
+ // session-store.ts:444-445 writes both as a pair from a server-derived stepped-up
136
+ // token, auth/refresh.ts:376 writes it as a MIRROR of mfaVerified, and
137
+ // routes/auth/session.ts:149-157 refuses both on PRESENCE, so a caller cannot
138
+ // assert either. The carry therefore only ever served sessions minted before
139
+ // `mfaVerified` existed, and for those the correct behaviour is re-verify, not
140
+ // grandfather. Cost is one forced re-challenge for a stale legacy session.
141
+ //
142
+ // Severity note, so nobody re-inflates this later: the operator was `??`
143
+ // (nullish), NOT `||`. `false ?? x` is `false`, so an explicitly DENIED session
144
+ // could never be upgraded by a stale flag. This was legacy grandfathering, not a
145
+ // live 2FA bypass. Had it been `||`, it would have been the bypass.
146
+ const mfaVerified = sessionData.mfaVerified ?? false;
147
+
148
+ // 339563 / 369858 §3: THIS IS STILL A FAIL-OPEN AND IS DELIBERATELY LEFT VISIBLE.
149
+ // An absent mfaExpiresAt collapses to 0, so `mfaExpired` is false and the MFA
150
+ // verdict NEVER EXPIRES. That needs no legacy-session hypothesis: it fires on a
151
+ // current `mfaVerified: true` session whose token carried no `mfa_expires` claim -
152
+ // the passwordless/primary-credential lane, named on the write path in the
153
+ // source's own voice at session-store.ts:464-467 ("it IS the case the consumers
154
+ // currently fail OPEN on"). Removing the carry above does NOT touch this.
155
+ // Tracked separately by BAPert; not fixed here because failing closed on absent
156
+ // expiry would deny the passwordless lane outright and that is a product call.
131
157
  const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
132
158
  const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
133
159
 
@@ -1,33 +0,0 @@
1
- export interface AuthContext {
2
- pathname: string;
3
- isPublicRoute: boolean;
4
- isLoginPage: boolean;
5
- searchParams: URLSearchParams;
6
- sessionPointer: {
7
- exists: boolean;
8
- sessionToken?: string;
9
- expired?: boolean;
10
- hasRefreshToken?: boolean;
11
- };
12
- sessionStatus: {
13
- exists: boolean | null;
14
- forceInvalid: boolean | null;
15
- requires2FA: boolean;
16
- twoFactorComplete: boolean;
17
- };
18
- circuitBreakerOpen: boolean;
19
- }
20
- export type AuthAction = {
21
- type: 'allow';
22
- } | {
23
- type: 'redirect';
24
- location: string;
25
- reason: string;
26
- clearCookies?: boolean;
27
- } | {
28
- type: 'service_error';
29
- reason: string;
30
- };
31
- export declare function makeAuthDecision(context: AuthContext): AuthAction;
32
- export declare function isLoginPage(pathname: string): boolean;
33
- export declare function getCallbackUrl(pathname: string, searchParams: URLSearchParams): string;
@@ -1,65 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.makeAuthDecision = makeAuthDecision;
4
- exports.isLoginPage = isLoginPage;
5
- exports.getCallbackUrl = getCallbackUrl;
6
- function makeAuthDecision(context) {
7
- const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
8
- // SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops
9
- const safeCallbackUrl = pathname.startsWith('/account-auth/') ? '/' : pathname;
10
- if (isPublicRoute && !isLoginPage)
11
- return { type: 'allow' };
12
- if (circuitBreakerOpen) {
13
- if (isLoginPage)
14
- return { type: 'allow' };
15
- return { type: 'redirect', location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen', reason: 'CircuitBreakerOpen', clearCookies: true };
16
- }
17
- if (!sessionPointer.exists) {
18
- if (isLoginPage)
19
- return { type: 'allow' };
20
- if (pathname === '/') {
21
- const unauthUrl = process.env.UNAUTHENTICATED_REDIRECT_URL || '/account-auth/login';
22
- return { type: 'redirect', location: unauthUrl, reason: 'unauthenticated_root_redirect' };
23
- }
24
- return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: 'no_session_pointer' };
25
- }
26
- if (sessionStatus.exists === null || sessionStatus.forceInvalid === null)
27
- return { type: 'service_error', reason: 'redis_unavailable' };
28
- if (sessionStatus.forceInvalid) {
29
- if (isLoginPage)
30
- return { type: 'allow' };
31
- return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=force_invalidated`, reason: 'force_invalidated' };
32
- }
33
- if (!sessionStatus.exists) {
34
- if (isLoginPage)
35
- return { type: 'allow' };
36
- return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=stale_session`, reason: 'stale_session' };
37
- }
38
- if (sessionStatus.requires2FA && !sessionStatus.twoFactorComplete) {
39
- if (pathname === '/account-auth/verify-code')
40
- return { type: 'allow' };
41
- if (isLoginPage) {
42
- const callbackUrl = searchParams.get('callbackUrl') || '/';
43
- const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl;
44
- return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' };
45
- }
46
- return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: '2fa_required' };
47
- }
48
- if (sessionPointer.expired) {
49
- if (isLoginPage)
50
- return { type: 'allow' };
51
- return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&error=SessionExpired`, reason: 'token_expired' };
52
- }
53
- if (pathname === '/')
54
- return { type: 'redirect', location: '/', reason: 'authenticated_root_redirect' };
55
- if (isLoginPage) {
56
- const callbackUrl = searchParams.get('callbackUrl') || '/';
57
- const safeCallbackUrl = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl;
58
- return { type: 'redirect', location: safeCallbackUrl, reason: 'already_authenticated' };
59
- }
60
- return { type: 'allow' };
61
- }
62
- function isLoginPage(pathname) { return pathname === '/account-auth/login'; }
63
- function getCallbackUrl(pathname, searchParams) { if (pathname.startsWith('/account-auth/'))
64
- return '/'; const existingCallback = searchParams.get('callbackUrl'); if (existingCallback && !existingCallback.startsWith('/account-auth/'))
65
- return existingCallback; return pathname; }
@@ -1,43 +0,0 @@
1
- /**
2
- * Auth Decision Engine (lifted)
3
- */
4
- import { NextRequest } from 'next/server';
5
-
6
- export interface AuthContext {
7
- pathname: string;
8
- isPublicRoute: boolean;
9
- isLoginPage: boolean;
10
- searchParams: URLSearchParams;
11
- sessionPointer: { exists: boolean; sessionToken?: string; expired?: boolean; hasRefreshToken?: boolean; };
12
- sessionStatus: { exists: boolean | null; forceInvalid: boolean | null; requires2FA: boolean; twoFactorComplete: boolean; };
13
- circuitBreakerOpen: boolean;
14
- }
15
-
16
- export type AuthAction =
17
- | { type: 'allow' }
18
- | { type: 'redirect'; location: string; reason: string; clearCookies?: boolean }
19
- | { type: 'service_error'; reason: string };
20
-
21
- export function makeAuthDecision(context: AuthContext): AuthAction {
22
- const { pathname, isPublicRoute, isLoginPage, searchParams, sessionPointer, sessionStatus, circuitBreakerOpen } = context;
23
- // SAFEGUARD: Never use auth pages as callback URLs to prevent redirect loops
24
- const safeCallbackUrl = pathname.startsWith('/account-auth/') ? '/' : pathname;
25
- if (isPublicRoute && !isLoginPage) return { type: 'allow' };
26
- if (circuitBreakerOpen) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: '/account-auth/login?error=ServiceUnavailable&reason=CircuitBreakerOpen', reason: 'CircuitBreakerOpen', clearCookies: true }; }
27
- if (!sessionPointer.exists) { if (isLoginPage) return { type: 'allow' }; if (pathname === '/') { const unauthUrl = process.env.UNAUTHENTICATED_REDIRECT_URL || '/account-auth/login'; return { type: 'redirect', location: unauthUrl, reason: 'unauthenticated_root_redirect' }; } return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: 'no_session_pointer' }; }
28
- if (sessionStatus.exists === null || sessionStatus.forceInvalid === null) return { type: 'service_error', reason: 'redis_unavailable' };
29
- if (sessionStatus.forceInvalid) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=force_invalidated`, reason: 'force_invalidated' }; }
30
- if (!sessionStatus.exists) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&reason=stale_session`, reason: 'stale_session' }; }
31
- if (sessionStatus.requires2FA && !sessionStatus.twoFactorComplete) {
32
- if (pathname === '/account-auth/verify-code') return { type: 'allow' };
33
- if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl2 = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl2)}`, reason: '2fa_required_login_redirect' }; }
34
- return { type: 'redirect', location: `/account-auth/verify-code?callbackUrl=${encodeURIComponent(safeCallbackUrl)}`, reason: '2fa_required' };
35
- }
36
- if (sessionPointer.expired) { if (isLoginPage) return { type: 'allow' }; return { type: 'redirect', location: `/account-auth/login?callbackUrl=${encodeURIComponent(safeCallbackUrl)}&error=SessionExpired`, reason: 'token_expired' }; }
37
- if (pathname === '/') return { type: 'redirect', location: '/', reason: 'authenticated_root_redirect' };
38
- if (isLoginPage) { const callbackUrl = searchParams.get('callbackUrl') || '/'; const safeCallbackUrl = callbackUrl.startsWith('/account-auth/') ? '/' : callbackUrl; return { type: 'redirect', location: safeCallbackUrl, reason: 'already_authenticated' }; }
39
- return { type: 'allow' };
40
- }
41
-
42
- export function isLoginPage(pathname: string): boolean { return pathname === '/account-auth/login'; }
43
- export function getCallbackUrl(pathname: string, searchParams: URLSearchParams): string { if (pathname.startsWith('/account-auth/')) return '/'; const existingCallback = searchParams.get('callbackUrl'); if (existingCallback && !existingCallback.startsWith('/account-auth/')) return existingCallback; return pathname; }