@payez/next-mvp 4.1.10 → 4.1.12

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 (89) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.js +9 -18
  3. package/dist/client/better-auth-client.d.ts +14 -2
  4. package/dist/lib/ensure-fresh-access-token.js +7 -30
  5. package/dist/lib/session-store.d.ts +0 -1
  6. package/dist/lib/session-store.js +29 -33
  7. package/dist/lib/startup-init.js +19 -19
  8. package/package.json +4 -3
  9. package/scripts/check-internal-url-usage.sh +73 -73
  10. package/src/api/index.ts +17 -17
  11. package/src/api-handlers/account/masked-info.ts +45 -45
  12. package/src/api-handlers/account/profile.ts +80 -80
  13. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  14. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  15. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  16. package/src/api-handlers/account/reset-password.ts +23 -23
  17. package/src/api-handlers/account/send-code.ts +76 -76
  18. package/src/api-handlers/account/validate-password.ts +118 -118
  19. package/src/api-handlers/admin/stats.ts +249 -249
  20. package/src/api-handlers/anon/preferences.ts +123 -123
  21. package/src/api-handlers/auth/jwks.ts +20 -20
  22. package/src/api-handlers/auth/login.ts +240 -240
  23. package/src/api-handlers/auth/refresh.ts +15 -38
  24. package/src/api-handlers/auth/validate.ts +43 -43
  25. package/src/api-handlers/session/viability.ts +142 -142
  26. package/src/auth/better-auth.ts +9 -14
  27. package/src/auth/unauthenticated-routes.ts +2 -2
  28. package/src/client/better-auth-client.ts +70 -70
  29. package/src/client/fetchWithSession.ts +21 -21
  30. package/src/client/index.ts +13 -13
  31. package/src/client/useAnonSession.ts +131 -131
  32. package/src/components/admin/VibeAdminContext.tsx +87 -87
  33. package/src/config/logging-config.ts +82 -82
  34. package/src/config/unauthenticated-routes.ts +19 -19
  35. package/src/edge/internal-api-url.ts +65 -65
  36. package/src/edge/middleware.ts +42 -42
  37. package/src/hooks/useProfile.ts +75 -75
  38. package/src/lib/anon-session.ts +213 -213
  39. package/src/lib/auth-secret.ts +137 -137
  40. package/src/lib/demo-mode.ts +17 -17
  41. package/src/lib/ensure-fresh-access-token.ts +7 -30
  42. package/src/lib/idp-client-config.ts +559 -559
  43. package/src/lib/idp-fetch.ts +126 -126
  44. package/src/lib/internal-api.ts +171 -171
  45. package/src/lib/jwt-decode-client.ts +45 -45
  46. package/src/lib/rate-limit-service.ts +9 -9
  47. package/src/lib/redis.ts +26 -26
  48. package/src/lib/refresh-token-validator.ts +64 -64
  49. package/src/lib/secret-validation.ts +8 -8
  50. package/src/lib/session-store.ts +692 -695
  51. package/src/lib/session.ts +34 -34
  52. package/src/lib/startup-init.ts +251 -251
  53. package/src/lib/token-expiry.ts +40 -40
  54. package/src/lib/token-lifecycle.ts +470 -470
  55. package/src/lib/types/api-responses.ts +336 -336
  56. package/src/middleware/auth-decision.ts +43 -43
  57. package/src/models/DecodedAccessToken.ts +16 -16
  58. package/src/models/SessionModel.ts +264 -264
  59. package/src/pages/admin-roles/index.ts +9 -9
  60. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  61. package/src/routes/account/index.ts +31 -31
  62. package/src/routes/account/masked-info.ts +36 -36
  63. package/src/routes/account/send-code.ts +39 -39
  64. package/src/routes/account/verify-email.ts +40 -40
  65. package/src/routes/account/verify-sms.ts +40 -40
  66. package/src/routes/auth/index.ts +22 -22
  67. package/src/routes/auth/refresh.ts +48 -48
  68. package/src/routes/auth/session.ts +166 -166
  69. package/src/routes/index.ts +17 -17
  70. package/src/routes/session/index.ts +7 -7
  71. package/src/routes/session/refresh-viability.ts +17 -17
  72. package/src/server/auth.ts +373 -373
  73. package/src/server/decode-session.ts +202 -202
  74. package/src/services/signalrActivityService.ts +258 -258
  75. package/src/theme/ThemeProvider.tsx +39 -39
  76. package/src/theme/default.ts +33 -33
  77. package/src/theme/index.ts +31 -31
  78. package/src/theme/types.ts +69 -69
  79. package/src/theme/useTheme.ts +57 -57
  80. package/src/theme/utils.ts +40 -40
  81. package/src/types/api.ts +12 -12
  82. package/src/types/auth.d.ts +23 -23
  83. package/src/types/auth.ts +22 -22
  84. package/src/types/logging.ts +11 -11
  85. package/src/types/recovery.ts +54 -54
  86. package/src/utils/api.ts +353 -353
  87. package/src/utils/circuitBreaker.ts +40 -40
  88. package/src/utils/error-message.ts +108 -108
  89. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,143 +1,143 @@
1
- /**
2
- * Session Viability Check API Handler for `@payez/next-mvp`
3
- *
4
- * This API route is called by the middleware to securely check if a session is valid.
5
- */
6
-
7
- import { NextRequest, NextResponse } from 'next/server';
8
- import { getSession as getRedisSession, getBetterAuthSession } from '../../lib/session-store';
9
- import { getSession } from '../../server/auth';
10
- import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
11
- import { getIDPClientConfig } from '../../lib/idp-client-config';
12
-
13
- export async function GET(req: NextRequest) {
14
- try {
15
- // Ensure initialization is complete (auth signing secret resolved from IDP)
16
- if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
17
- try {
18
- await ensureInitialized();
19
- } catch (error) {
20
- // Initialization failed - return 503
21
- console.error('[API Viability] Initialization failed - returning 503');
22
- return NextResponse.json(
23
- {
24
- error: 'Service Unavailable',
25
- message: 'Authentication service is not properly configured',
26
- code: 'AUTH_NOT_INITIALIZED'
27
- },
28
- { status: 503 }
29
- );
30
- }
31
- }
32
-
33
- // Double-check after initialization attempt
34
- if (isInitializationFailed()) {
35
- console.error('[API Viability] Initialization failed - returning 503');
36
- return NextResponse.json(
37
- {
38
- error: 'Service Unavailable',
39
- message: 'Authentication service is not properly configured',
40
- code: 'AUTH_NOT_INITIALIZED'
41
- },
42
- { status: 503 }
43
- );
44
- }
45
-
46
- // Get session from Better Auth
47
- const betterAuthSession = await getSession(req);
48
-
49
- // Debug logging
50
- if (!betterAuthSession) {
51
- console.warn('[VIABILITY] getSession returned null');
52
- }
53
-
54
- const sessionToken = betterAuthSession?.session?.token as string | undefined;
55
- if (betterAuthSession && sessionToken) {
56
- // Try legacy session store first, then Better Auth format
57
- let sessionData = await getRedisSession(sessionToken);
58
- if (!sessionData) {
59
- // Better Auth stores sessions with ba:{appSlug}:{token} prefix
60
- sessionData = await getBetterAuthSession(sessionToken);
61
- if (sessionData) {
62
- console.log('[VIABILITY] Found session in Better Auth store');
63
- }
64
- }
65
- if (sessionData) {
66
- // The session exists in Redis
67
-
68
- // Check if access token is expired (for middleware decision-making)
69
- const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
70
- const accessTokenExpired = accessTokenExpires < Date.now();
71
-
72
- // Get requires2FA from cached client config (client-wide setting)
73
- let requires2FA = true; // Default to true for security
74
- try {
75
- const cachedConfig = await getIDPClientConfig();
76
- requires2FA = cachedConfig.authSettings?.require2FA ?? true;
77
- } catch (e) {
78
- console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
79
- }
80
-
81
- // CRITICAL: OAuth sessions without refresh token always need 2FA completion
82
- // to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
83
- // when no refresh_token is present. This must override the client config.
84
- if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
85
- requires2FA = true;
86
- }
87
-
88
- // CRITICAL: Check if MFA has expired (2FA TTL enforcement)
89
- // The session may have mfaVerified=true from days ago, but if mfaExpiresAt
90
- // has passed, we must treat 2FA as incomplete to force re-verification.
91
- const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
92
- const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
93
- // Check both field names for compatibility (mfaVerified is the normalized name)
94
- const sessionMfaComplete = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
95
- const effectiveTwoFactorComplete = sessionMfaComplete && !mfaExpired;
96
-
97
- console.log('[VIABILITY] Session 2FA check:', {
98
- sessionToken: sessionToken.substring(0, 8) + '...',
99
- mfaVerified: sessionData.mfaVerified,
100
- twoFactorComplete: (sessionData as any).twoFactorComplete,
101
- sessionMfaComplete,
102
- mfaExpired,
103
- effectiveTwoFactorComplete,
104
- });
105
-
106
- if (mfaExpired && sessionMfaComplete) {
107
- console.warn('[API Viability] MFA expired - forcing 2FA re-verification', {
108
- mfaExpiresAt: new Date(mfaExpiresAt).toISOString(),
109
- now: new Date().toISOString(),
110
- hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
111
- });
112
- }
113
-
114
- const response = {
115
- authenticated: true,
116
- sessionToken, // Include token for middleware tracking
117
- // 2FA fields - critical for middleware redirect logic
118
- requires2FA, // From cached client config (client-wide setting)
119
- twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
120
- // Token status for refresh decisions
121
- accessTokenExpired,
122
- hasRefreshToken: !!sessionData.idpRefreshToken
123
- };
124
- return NextResponse.json(response);
125
- }
126
-
127
- // CRITICAL: Cookie exists but Redis session is missing (stale cookie state)
128
- // Return sessionToken so middleware can detect this and clear the stale cookie
129
- console.warn('[VIABILITY] Stale cookie detected - session not in Redis');
130
- return NextResponse.json({
131
- authenticated: false,
132
- sessionToken // Include token to enable stale cookie detection
133
- });
134
- }
135
-
136
- // If there's no token at all, it's not authenticated
137
- return NextResponse.json({ authenticated: false });
138
-
139
- } catch (error) {
140
- console.error('[API Viability] Error checking session viability:', error);
141
- return NextResponse.json({ authenticated: false }, { status: 500 });
142
- }
1
+ /**
2
+ * Session Viability Check API Handler for `@payez/next-mvp`
3
+ *
4
+ * This API route is called by the middleware to securely check if a session is valid.
5
+ */
6
+
7
+ import { NextRequest, NextResponse } from 'next/server';
8
+ import { getSession as getRedisSession, getBetterAuthSession } from '../../lib/session-store';
9
+ import { getSession } from '../../server/auth';
10
+ import { isInitializationFailed, ensureInitialized } from '../../lib/startup-init';
11
+ import { getIDPClientConfig } from '../../lib/idp-client-config';
12
+
13
+ export async function GET(req: NextRequest) {
14
+ try {
15
+ // Ensure initialization is complete (auth signing secret resolved from IDP)
16
+ if (!process.env.BETTER_AUTH_SECRET && !process.env.NEXTAUTH_SECRET) {
17
+ try {
18
+ await ensureInitialized();
19
+ } catch (error) {
20
+ // Initialization failed - return 503
21
+ console.error('[API Viability] Initialization failed - returning 503');
22
+ return NextResponse.json(
23
+ {
24
+ error: 'Service Unavailable',
25
+ message: 'Authentication service is not properly configured',
26
+ code: 'AUTH_NOT_INITIALIZED'
27
+ },
28
+ { status: 503 }
29
+ );
30
+ }
31
+ }
32
+
33
+ // Double-check after initialization attempt
34
+ if (isInitializationFailed()) {
35
+ console.error('[API Viability] Initialization failed - returning 503');
36
+ return NextResponse.json(
37
+ {
38
+ error: 'Service Unavailable',
39
+ message: 'Authentication service is not properly configured',
40
+ code: 'AUTH_NOT_INITIALIZED'
41
+ },
42
+ { status: 503 }
43
+ );
44
+ }
45
+
46
+ // Get session from Better Auth
47
+ const betterAuthSession = await getSession(req);
48
+
49
+ // Debug logging
50
+ if (!betterAuthSession) {
51
+ console.warn('[VIABILITY] getSession returned null');
52
+ }
53
+
54
+ const sessionToken = betterAuthSession?.session?.token as string | undefined;
55
+ if (betterAuthSession && sessionToken) {
56
+ // Try legacy session store first, then Better Auth format
57
+ let sessionData = await getRedisSession(sessionToken);
58
+ if (!sessionData) {
59
+ // Better Auth stores sessions with ba:{appSlug}:{token} prefix
60
+ sessionData = await getBetterAuthSession(sessionToken);
61
+ if (sessionData) {
62
+ console.log('[VIABILITY] Found session in Better Auth store');
63
+ }
64
+ }
65
+ if (sessionData) {
66
+ // The session exists in Redis
67
+
68
+ // Check if access token is expired (for middleware decision-making)
69
+ const accessTokenExpires = sessionData.idpAccessTokenExpires || 0;
70
+ const accessTokenExpired = accessTokenExpires < Date.now();
71
+
72
+ // Get requires2FA from cached client config (client-wide setting)
73
+ let requires2FA = true; // Default to true for security
74
+ try {
75
+ const cachedConfig = await getIDPClientConfig();
76
+ requires2FA = cachedConfig.authSettings?.require2FA ?? true;
77
+ } catch (e) {
78
+ console.warn('[API Viability] Could not get client config, defaulting requires2FA to true');
79
+ }
80
+
81
+ // CRITICAL: OAuth sessions without refresh token always need 2FA completion
82
+ // to obtain a refresh token from the IDP. The OAuth callback sets mfaVerified=false
83
+ // when no refresh_token is present. This must override the client config.
84
+ if (sessionData.mfaVerified === false && !sessionData.idpRefreshToken) {
85
+ requires2FA = true;
86
+ }
87
+
88
+ // CRITICAL: Check if MFA has expired (2FA TTL enforcement)
89
+ // The session may have mfaVerified=true from days ago, but if mfaExpiresAt
90
+ // has passed, we must treat 2FA as incomplete to force re-verification.
91
+ const mfaExpiresAt = sessionData.mfaExpiresAt || 0;
92
+ const mfaExpired = mfaExpiresAt > 0 && mfaExpiresAt < Date.now();
93
+ // Check both field names for compatibility (mfaVerified is the normalized name)
94
+ const sessionMfaComplete = sessionData.mfaVerified ?? (sessionData as any).twoFactorComplete ?? false;
95
+ const effectiveTwoFactorComplete = sessionMfaComplete && !mfaExpired;
96
+
97
+ console.log('[VIABILITY] Session 2FA check:', {
98
+ sessionToken: sessionToken.substring(0, 8) + '...',
99
+ mfaVerified: sessionData.mfaVerified,
100
+ twoFactorComplete: (sessionData as any).twoFactorComplete,
101
+ sessionMfaComplete,
102
+ mfaExpired,
103
+ effectiveTwoFactorComplete,
104
+ });
105
+
106
+ if (mfaExpired && sessionMfaComplete) {
107
+ console.warn('[API Viability] MFA expired - forcing 2FA re-verification', {
108
+ mfaExpiresAt: new Date(mfaExpiresAt).toISOString(),
109
+ now: new Date().toISOString(),
110
+ hoursExpiredAgo: ((Date.now() - mfaExpiresAt) / (1000 * 60 * 60)).toFixed(1)
111
+ });
112
+ }
113
+
114
+ const response = {
115
+ authenticated: true,
116
+ sessionToken, // Include token for middleware tracking
117
+ // 2FA fields - critical for middleware redirect logic
118
+ requires2FA, // From cached client config (client-wide setting)
119
+ twoFactorComplete: effectiveTwoFactorComplete, // From session, BUT respects MFA TTL
120
+ // Token status for refresh decisions
121
+ accessTokenExpired,
122
+ hasRefreshToken: !!sessionData.idpRefreshToken
123
+ };
124
+ return NextResponse.json(response);
125
+ }
126
+
127
+ // CRITICAL: Cookie exists but Redis session is missing (stale cookie state)
128
+ // Return sessionToken so middleware can detect this and clear the stale cookie
129
+ console.warn('[VIABILITY] Stale cookie detected - session not in Redis');
130
+ return NextResponse.json({
131
+ authenticated: false,
132
+ sessionToken // Include token to enable stale cookie detection
133
+ });
134
+ }
135
+
136
+ // If there's no token at all, it's not authenticated
137
+ return NextResponse.json({ authenticated: false });
138
+
139
+ } catch (error) {
140
+ console.error('[API Viability] Error checking session viability:', error);
141
+ return NextResponse.json({ authenticated: false }, { status: 500 });
142
+ }
143
143
  }
@@ -18,7 +18,6 @@ import type { IDPClientConfig } from '../lib/idp-client-config';
18
18
  import { getIDPClientConfig } from '../lib/idp-client-config';
19
19
  import { getAppSlug } from '../lib/app-slug';
20
20
  import { getRedis } from '../lib/redis';
21
- import { SESSION_TTL } from '../lib/session-store';
22
21
 
23
22
  /**
24
23
  * Better Auth social provider config shape.
@@ -124,19 +123,15 @@ export function createBetterAuthInstance(
124
123
  // Redis-backed session storage via secondaryStorage
125
124
  secondaryStorage: {
126
125
  get: async (key: string) => {
127
- try {
128
- return await getRedis().get(`ba:${appSlug}:${key}`);
129
- } catch { return null; }
126
+ return await getRedis().get(`ba:${appSlug}:${key}`);
130
127
  },
131
128
  set: async (key: string, value: string, ttl?: number) => {
132
- try {
133
- const redis = getRedis();
134
- if (ttl) {
135
- await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
136
- } else {
137
- await redis.setex(`ba:${appSlug}:${key}`, SESSION_TTL, value);
138
- }
139
- } catch { /* Redis unavailable — cookie cache still works */ }
129
+ const redis = getRedis();
130
+ if (ttl) {
131
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
132
+ } else {
133
+ await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
134
+ }
140
135
  },
141
136
  delete: async (key: string) => {
142
137
  try {
@@ -365,14 +360,14 @@ export async function exchangeOAuthForIdpTokens(
365
360
 
366
361
  // Store in BA Redis session (for decodeSession)
367
362
  baData.idpTokens = idpTokenData;
368
- await getRedis().setex(baKey, SESSION_TTL, JSON.stringify(baData));
363
+ await getRedis().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
369
364
 
370
365
  // Write to canonical session store so refresh handler and token lifecycle can find the tokens.
371
366
  // Key format: {sessionPrefix}{token} — same key that getSession() reads from.
372
367
  try {
373
368
  const { getSessionPrefix } = await import('../lib/app-slug');
374
369
  const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
375
- await getRedis().setex(canonicalKey, SESSION_TTL, JSON.stringify({
370
+ await getRedis().setex(canonicalKey, 7 * 24 * 60 * 60, JSON.stringify({
376
371
  ...idpTokenData,
377
372
  oauthProvider: provider,
378
373
  }));
@@ -1,3 +1,3 @@
1
- // src/auth/unauthenticated-routes.ts
2
- // Re-exporting from route-config for backward compatibility
1
+ // src/auth/unauthenticated-routes.ts
2
+ // Re-exporting from route-config for backward compatibility
3
3
  export * from './route-config';
@@ -1,70 +1,70 @@
1
- /**
2
- * Better Auth Client.
3
- *
4
- * Import from '@payez/next-mvp/client/better-auth-client'.
5
- *
6
- * Includes useSessionCompat() — returns a { data, status } shape so existing
7
- * components written against the legacy hook don't need destructure changes.
8
- */
9
-
10
- import { createAuthClient } from 'better-auth/react';
11
- import { magicLinkClient } from 'better-auth/client/plugins';
12
- import { useMemo } from 'react';
13
-
14
- export const authClient = createAuthClient({
15
- // baseURL derived from BETTER_AUTH_URL or window.location.origin
16
- plugins: [magicLinkClient()],
17
- });
18
-
19
- // Convenience exports
20
- export const { useSession, signIn, signOut } = authClient;
21
-
22
-
23
- /**
24
- * NextAuth-compatible useSession wrapper.
25
- *
26
- * Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
27
- * Drop-in replacement — no destructure changes needed in consuming components.
28
- */
29
- export function useSessionCompat() {
30
- const baSession = authClient.useSession();
31
-
32
- const status = useMemo(() => {
33
- if (baSession.isPending) return 'loading' as const;
34
- if (baSession.data) return 'authenticated' as const;
35
- return 'unauthenticated' as const;
36
- }, [baSession.isPending, baSession.data]);
37
-
38
- // Map Better Auth session shape to NextAuth session shape
39
- const data = useMemo(() => {
40
- if (!baSession.data) return null;
41
- return {
42
- ...baSession.data,
43
- user: baSession.data.user,
44
- expires: '', // Better Auth handles expiry differently
45
- };
46
- }, [baSession.data]);
47
-
48
- return {
49
- data,
50
- status,
51
- update: async () => {
52
- // Better Auth doesn't have a direct "refresh session" call.
53
- // Force refetch by invalidating the query.
54
- // TODO: Wire to proper session refresh when available.
55
- return data;
56
- },
57
- };
58
- }
59
-
60
- /**
61
- * NextAuth-compatible signOut wrapper.
62
- *
63
- * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
64
- */
65
- export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
66
- await authClient.signOut();
67
- if (options?.redirect !== false && options?.callbackUrl) {
68
- window.location.href = options.callbackUrl;
69
- }
70
- }
1
+ /**
2
+ * Better Auth Client.
3
+ *
4
+ * Import from '@payez/next-mvp/client/better-auth-client'.
5
+ *
6
+ * Includes useSessionCompat() — returns a { data, status } shape so existing
7
+ * components written against the legacy hook don't need destructure changes.
8
+ */
9
+
10
+ import { createAuthClient } from 'better-auth/react';
11
+ import { magicLinkClient } from 'better-auth/client/plugins';
12
+ import { useMemo } from 'react';
13
+
14
+ export const authClient = createAuthClient({
15
+ // baseURL derived from BETTER_AUTH_URL or window.location.origin
16
+ plugins: [magicLinkClient()],
17
+ });
18
+
19
+ // Convenience exports
20
+ export const { useSession, signIn, signOut } = authClient;
21
+
22
+
23
+ /**
24
+ * NextAuth-compatible useSession wrapper.
25
+ *
26
+ * Maps Better Auth's { data, error, isPending } to NextAuth's { data, status, update }.
27
+ * Drop-in replacement — no destructure changes needed in consuming components.
28
+ */
29
+ export function useSessionCompat() {
30
+ const baSession = authClient.useSession();
31
+
32
+ const status = useMemo(() => {
33
+ if (baSession.isPending) return 'loading' as const;
34
+ if (baSession.data) return 'authenticated' as const;
35
+ return 'unauthenticated' as const;
36
+ }, [baSession.isPending, baSession.data]);
37
+
38
+ // Map Better Auth session shape to NextAuth session shape
39
+ const data = useMemo(() => {
40
+ if (!baSession.data) return null;
41
+ return {
42
+ ...baSession.data,
43
+ user: baSession.data.user,
44
+ expires: '', // Better Auth handles expiry differently
45
+ };
46
+ }, [baSession.data]);
47
+
48
+ return {
49
+ data,
50
+ status,
51
+ update: async () => {
52
+ // Better Auth doesn't have a direct "refresh session" call.
53
+ // Force refetch by invalidating the query.
54
+ // TODO: Wire to proper session refresh when available.
55
+ return data;
56
+ },
57
+ };
58
+ }
59
+
60
+ /**
61
+ * NextAuth-compatible signOut wrapper.
62
+ *
63
+ * Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
64
+ */
65
+ export async function signOutCompat(options?: { redirect?: boolean; callbackUrl?: string }) {
66
+ await authClient.signOut();
67
+ if (options?.redirect !== false && options?.callbackUrl) {
68
+ window.location.href = options.callbackUrl;
69
+ }
70
+ }
@@ -1,21 +1,21 @@
1
- import { useRef } from 'react';
2
-
3
- export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
4
- const retry = opts.retry ?? 1;
5
- const doFetch = async (): Promise<Response> => {
6
- const res = await fetch(input, { ...init, credentials: 'include' });
7
- if (res.ok) return res;
8
- if (res.status === 401 && retry > 0) {
9
- const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
10
- if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
11
- }
12
- if ((res.status === 409 || res.status === 503) && retry > 0) {
13
- const retryAfter = res.headers.get('Retry-After');
14
- const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
15
- await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
16
- return fetchWithSession(input, init, { retry: retry - 1 });
17
- }
18
- return res;
19
- };
20
- return doFetch();
21
- }
1
+ import { useRef } from 'react';
2
+
3
+ export async function fetchWithSession(input: RequestInfo | URL, init: RequestInit = {}, opts: { retry?: number } = {}): Promise<Response> {
4
+ const retry = opts.retry ?? 1;
5
+ const doFetch = async (): Promise<Response> => {
6
+ const res = await fetch(input, { ...init, credentials: 'include' });
7
+ if (res.ok) return res;
8
+ if (res.status === 401 && retry > 0) {
9
+ const rf = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Accept': 'application/json' }, credentials: 'include' });
10
+ if (rf.ok) return fetchWithSession(input, init, { retry: retry - 1 });
11
+ }
12
+ if ((res.status === 409 || res.status === 503) && retry > 0) {
13
+ const retryAfter = res.headers.get('Retry-After');
14
+ const waitMs = retryAfter ? parseInt(retryAfter) * 1000 : 1000;
15
+ await new Promise(r => setTimeout(r, Math.min(waitMs, 3000)));
16
+ return fetchWithSession(input, init, { retry: retry - 1 });
17
+ }
18
+ return res;
19
+ };
20
+ return doFetch();
21
+ }
@@ -1,13 +1,13 @@
1
- /**
2
- * Client-Side Exports
3
- *
4
- * This module exports only client-safe code for use in browser environments.
5
- * Server-only utilities and Node.js dependencies are excluded.
6
- */
7
-
8
- // Client-side fetch utility
9
- export { fetchWithAuth } from './fetch-with-auth';
10
-
11
- // Authentication context and hooks
12
- export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
13
- export type { AuthConfig } from '../types/auth';
1
+ /**
2
+ * Client-Side Exports
3
+ *
4
+ * This module exports only client-safe code for use in browser environments.
5
+ * Server-only utilities and Node.js dependencies are excluded.
6
+ */
7
+
8
+ // Client-side fetch utility
9
+ export { fetchWithAuth } from './fetch-with-auth';
10
+
11
+ // Authentication context and hooks
12
+ export { AuthProvider, useAuthConfig, useAuthMode, useFederatedProviders, useFederatedAuthEnabled, useTraditionalAuthEnabled } from './AuthContext';
13
+ export type { AuthConfig } from '../types/auth';