@payez/next-mvp 4.0.6 → 4.0.8

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.
@@ -33,11 +33,15 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): im
33
33
  secret: string;
34
34
  socialProviders: Record<string, BetterAuthSocialProvider>;
35
35
  trustedOrigins: string[];
36
+ secondaryStorage: {
37
+ get: (key: string) => Promise<string | null>;
38
+ set: (key: string, value: string, ttl?: number) => Promise<void>;
39
+ delete: (key: string) => Promise<void>;
40
+ };
36
41
  session: {
37
42
  cookieCache: {
38
43
  enabled: true;
39
44
  maxAge: number;
40
- refreshCache: true;
41
45
  };
42
46
  };
43
47
  advanced: {
@@ -66,6 +70,13 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): im
66
70
  * Better Auth is always enabled (NextAuth removed in 4.0).
67
71
  */
68
72
  export declare function isBetterAuthEnabled(): boolean;
73
+ /**
74
+ * Get Better Auth Next.js route handlers (GET, POST).
75
+ * Initializes Better Auth from IDP config on first call, caches the instance.
76
+ */
77
+ declare let cachedInstance: any;
78
+ export { cachedInstance as __betterAuthInstance };
79
+ export declare function getBetterAuthInstance(): Promise<any>;
69
80
  /**
70
81
  * Get flag-gated auth handler for Next.js route.
71
82
  *
@@ -10,9 +10,11 @@
10
10
  * @see BETTER-AUTH-MIGRATION-SPEC.md
11
11
  */
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.__betterAuthInstance = void 0;
13
14
  exports.buildBetterAuthProviders = buildBetterAuthProviders;
14
15
  exports.createBetterAuthInstance = createBetterAuthInstance;
15
16
  exports.isBetterAuthEnabled = isBetterAuthEnabled;
17
+ exports.getBetterAuthInstance = getBetterAuthInstance;
16
18
  exports.getBetterAuthHandler = getBetterAuthHandler;
17
19
  require("server-only");
18
20
  const better_auth_1 = require("better-auth");
@@ -20,6 +22,7 @@ const next_js_1 = require("better-auth/next-js");
20
22
  const next_js_2 = require("better-auth/next-js");
21
23
  const idp_client_config_1 = require("../lib/idp-client-config");
22
24
  const app_slug_1 = require("../lib/app-slug");
25
+ const redis_1 = require("../lib/redis");
23
26
  /**
24
27
  * Build Better Auth social providers from IDP config.
25
28
  */
@@ -61,13 +64,39 @@ function createBetterAuthInstance(idpConfig) {
61
64
  'http://localhost:3400',
62
65
  'http://localhost:3600',
63
66
  ],
64
- // No database stateless mode. Better Auth defaults to JWE cookie cache.
65
- // Session cookie cache with refreshCache for DB-less setup.
67
+ // Redis-backed session storage via secondaryStorage
68
+ secondaryStorage: {
69
+ get: async (key) => {
70
+ try {
71
+ return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ },
77
+ set: async (key, value, ttl) => {
78
+ try {
79
+ const redis = (0, redis_1.getRedis)();
80
+ if (ttl) {
81
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
82
+ }
83
+ else {
84
+ await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
85
+ }
86
+ }
87
+ catch { /* Redis unavailable — cookie cache still works */ }
88
+ },
89
+ delete: async (key) => {
90
+ try {
91
+ await (0, redis_1.getRedis)().del(`ba:${appSlug}:${key}`);
92
+ }
93
+ catch { /* ignore */ }
94
+ },
95
+ },
66
96
  session: {
67
97
  cookieCache: {
68
98
  enabled: true,
69
99
  maxAge: 300,
70
- refreshCache: true,
71
100
  },
72
101
  },
73
102
  // Cookie prefix must match slim-middleware expectations ({slug}.session-token)
@@ -96,6 +125,7 @@ function isBetterAuthEnabled() {
96
125
  */
97
126
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
127
  let cachedInstance = null;
128
+ exports.__betterAuthInstance = cachedInstance;
99
129
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
130
  let initPromise = null;
101
131
  async function getBetterAuthInstance() {
@@ -104,7 +134,7 @@ async function getBetterAuthInstance() {
104
134
  if (!initPromise) {
105
135
  initPromise = (0, idp_client_config_1.getIDPClientConfig)().then(config => {
106
136
  const instance = createBetterAuthInstance(config);
107
- cachedInstance = instance;
137
+ exports.__betterAuthInstance = cachedInstance = instance;
108
138
  console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
109
139
  return instance;
110
140
  });
@@ -16,11 +16,15 @@ export declare function getAuthInstance(): Promise<import("better-auth/types").A
16
16
  secret: string;
17
17
  socialProviders: Record<string, import("../auth/better-auth").BetterAuthSocialProvider>;
18
18
  trustedOrigins: string[];
19
+ secondaryStorage: {
20
+ get: (key: string) => Promise<string | null>;
21
+ set: (key: string, value: string, ttl?: number) => Promise<void>;
22
+ delete: (key: string) => Promise<void>;
23
+ };
19
24
  session: {
20
25
  cookieCache: {
21
26
  enabled: true;
22
27
  maxAge: number;
23
- refreshCache: true;
24
28
  };
25
29
  };
26
30
  advanced: {
@@ -1,10 +1,8 @@
1
1
  /**
2
2
  * Server-Side Session Decoder
3
3
  *
4
- * Reads the JWT session cookie, decodes it with jose, and fetches the
5
- * full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
6
- *
7
- * Zero HTTP self-fetches. Direct Redis reads only.
4
+ * Uses Better Auth's server-side session API to get the current session.
5
+ * Falls back to legacy JWT + Redis path if Better Auth session not found.
8
6
  */
9
7
  import 'server-only';
10
8
  import { type JWTPayload } from 'jose';
@@ -17,8 +15,8 @@ export interface DecodedSession {
17
15
  };
18
16
  }
19
17
  /**
20
- * Decode the session from cookies and Redis.
21
- * Returns null if no valid session exists.
18
+ * Decode the session from cookies.
19
+ * Tries Better Auth first, falls back to legacy JWT + Redis.
22
20
  *
23
21
  * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
24
22
  * If omitted, uses next/headers cookies() for server components.
@@ -2,11 +2,42 @@
2
2
  /**
3
3
  * Server-Side Session Decoder
4
4
  *
5
- * Reads the JWT session cookie, decodes it with jose, and fetches the
6
- * full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
7
- *
8
- * Zero HTTP self-fetches. Direct Redis reads only.
5
+ * Uses Better Auth's server-side session API to get the current session.
6
+ * Falls back to legacy JWT + Redis path if Better Auth session not found.
9
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
10
41
  Object.defineProperty(exports, "__esModule", { value: true });
11
42
  exports.decodeSession = decodeSession;
12
43
  require("server-only");
@@ -17,17 +48,100 @@ const idp_client_config_1 = require("../lib/idp-client-config");
17
48
  const app_slug_1 = require("../lib/app-slug");
18
49
  const startup_init_1 = require("../lib/startup-init");
19
50
  /**
20
- * Decode the session from cookies and Redis.
21
- * Returns null if no valid session exists.
51
+ * Try Better Auth's server-side session API.
52
+ * Returns a DecodedSession if Better Auth has an active session, null otherwise.
53
+ */
54
+ async function tryBetterAuthSession(requestCookies) {
55
+ try {
56
+ const { getBetterAuthHandler } = await Promise.resolve().then(() => __importStar(require('../auth/better-auth')));
57
+ // getBetterAuthHandler initializes the instance; we need the raw instance
58
+ const { default: getBetterAuthInstanceFn } = await Promise.resolve().then(() => __importStar(require('../auth/better-auth'))).then(m => ({ default: m.getBetterAuthInstance || null }))
59
+ .catch(() => ({ default: null }));
60
+ // Access the cached instance via the module's internal getter
61
+ let auth = null;
62
+ try {
63
+ // Force handler init which caches the instance, then use the API
64
+ await getBetterAuthHandler();
65
+ // The instance is cached in the module — re-import to access it
66
+ const mod = await Promise.resolve().then(() => __importStar(require('../auth/better-auth')));
67
+ auth = mod.__betterAuthInstance;
68
+ }
69
+ catch {
70
+ return null;
71
+ }
72
+ if (!auth?.api?.getSession) {
73
+ return null;
74
+ }
75
+ // Build headers from cookies for Better Auth to read
76
+ const cookieStore = requestCookies || (await (0, headers_1.cookies)());
77
+ const headerObj = new Headers();
78
+ // Collect all cookies into a Cookie header
79
+ if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
80
+ const allCookies = cookieStore.getAll();
81
+ const cookieStr = allCookies.map((c) => `${c.name}=${c.value}`).join('; ');
82
+ headerObj.set('cookie', cookieStr);
83
+ }
84
+ else {
85
+ // Fallback: read known cookie names
86
+ const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
87
+ const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
88
+ const parts = [];
89
+ const sc = cookieStore.get(secureCookieName);
90
+ if (sc?.value)
91
+ parts.push(`${secureCookieName}=${sc.value}`);
92
+ const nc = cookieStore.get(sessionCookieName);
93
+ if (nc?.value)
94
+ parts.push(`${sessionCookieName}=${nc.value}`);
95
+ if (parts.length > 0)
96
+ headerObj.set('cookie', parts.join('; '));
97
+ }
98
+ const result = await auth.api.getSession({ headers: headerObj });
99
+ if (!result?.session || !result?.user) {
100
+ return null;
101
+ }
102
+ // Map Better Auth session to SessionData
103
+ const sessionData = {
104
+ userId: result.user.id || '',
105
+ email: result.user.email || '',
106
+ name: result.user.name || undefined,
107
+ roles: [],
108
+ idpAccessTokenExpires: result.session.expiresAt
109
+ ? new Date(result.session.expiresAt).getTime()
110
+ : Date.now() + 24 * 60 * 60 * 1000,
111
+ mfaVerified: true, // Social login doesn't require MFA
112
+ oauthProvider: 'google',
113
+ };
114
+ const jwtPayload = {
115
+ sub: result.user.id,
116
+ email: result.user.email,
117
+ name: result.user.name,
118
+ iat: Math.floor(Date.now() / 1000),
119
+ exp: sessionData.idpAccessTokenExpires / 1000,
120
+ sessionToken: result.session.token,
121
+ };
122
+ return { sessionData, jwtPayload };
123
+ }
124
+ catch (error) {
125
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Decode the session from cookies.
131
+ * Tries Better Auth first, falls back to legacy JWT + Redis.
22
132
  *
23
133
  * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
24
134
  * If omitted, uses next/headers cookies() for server components.
25
135
  */
26
136
  async function decodeSession(requestCookies) {
27
137
  try {
28
- // Ensure startup initialization is complete (Redis, IDP config, etc.)
29
138
  await (0, startup_init_1.ensureInitialized)();
30
- // Get the JWT cookie value
139
+ // Try Better Auth session first
140
+ const betterAuthSession = await tryBetterAuthSession(requestCookies);
141
+ if (betterAuthSession) {
142
+ return betterAuthSession;
143
+ }
144
+ // Fall back to legacy JWT + Redis path
31
145
  const cookieStore = requestCookies || (await (0, headers_1.cookies)());
32
146
  const sessionCookieName = (0, app_slug_1.getSessionCookieName)();
33
147
  const secureCookieName = (0, app_slug_1.getSecureSessionCookieName)();
@@ -36,14 +150,12 @@ async function decodeSession(requestCookies) {
36
150
  if (!cookieValue) {
37
151
  return null;
38
152
  }
39
- // Get the NextAuth secret from IDP config
40
153
  const config = await (0, idp_client_config_1.getIDPClientConfig)();
41
154
  const secret = config.nextAuthSecret;
42
155
  if (!secret) {
43
156
  console.error('[DECODE-SESSION] No nextAuthSecret available from IDP config');
44
157
  return null;
45
158
  }
46
- // Decode the JWT (same pattern as test-aware-get-token.ts)
47
159
  const secretKey = new TextEncoder().encode(secret);
48
160
  let payload;
49
161
  try {
@@ -51,17 +163,14 @@ async function decodeSession(requestCookies) {
51
163
  payload = result.payload;
52
164
  }
53
165
  catch (jwtError) {
54
- // JWT decode failed - cookie may be corrupted or secret rotated
55
166
  console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
56
167
  return null;
57
168
  }
58
- // Extract the Redis session ID from JWT payload
59
169
  const sessionToken = payload.sessionToken || payload.redisSessionId;
60
170
  if (!sessionToken) {
61
171
  console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
62
172
  return null;
63
173
  }
64
- // Fetch session from Redis (direct, no HTTP)
65
174
  const sessionData = await (0, session_store_1.getSession)(sessionToken);
66
175
  if (!sessionData) {
67
176
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.0.6",
3
+ "version": "4.0.8",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -16,6 +16,7 @@ import { toNextJsHandler } from 'better-auth/next-js';
16
16
  import type { IDPClientConfig } from '../lib/idp-client-config';
17
17
  import { getIDPClientConfig } from '../lib/idp-client-config';
18
18
  import { getAppSlug } from '../lib/app-slug';
19
+ import { getRedis } from '../lib/redis';
19
20
 
20
21
  /**
21
22
  * Better Auth social provider config shape.
@@ -76,13 +77,34 @@ export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
76
77
  'http://localhost:3600',
77
78
  ],
78
79
 
79
- // No database stateless mode. Better Auth defaults to JWE cookie cache.
80
- // Session cookie cache with refreshCache for DB-less setup.
80
+ // Redis-backed session storage via secondaryStorage
81
+ secondaryStorage: {
82
+ get: async (key: string) => {
83
+ try {
84
+ return await getRedis().get(`ba:${appSlug}:${key}`);
85
+ } catch { return null; }
86
+ },
87
+ set: async (key: string, value: string, ttl?: number) => {
88
+ try {
89
+ const redis = getRedis();
90
+ if (ttl) {
91
+ await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
92
+ } else {
93
+ await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
94
+ }
95
+ } catch { /* Redis unavailable — cookie cache still works */ }
96
+ },
97
+ delete: async (key: string) => {
98
+ try {
99
+ await getRedis().del(`ba:${appSlug}:${key}`);
100
+ } catch { /* ignore */ }
101
+ },
102
+ },
103
+
81
104
  session: {
82
105
  cookieCache: {
83
106
  enabled: true,
84
107
  maxAge: 300,
85
- refreshCache: true,
86
108
  },
87
109
  },
88
110
 
@@ -118,7 +140,11 @@ let cachedInstance: any = null;
118
140
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
141
  let initPromise: Promise<any> | null = null;
120
142
 
121
- async function getBetterAuthInstance() {
143
+ // Expose for server-side session access (decode-session.ts)
144
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
145
+ export { cachedInstance as __betterAuthInstance };
146
+
147
+ export async function getBetterAuthInstance() {
122
148
  if (cachedInstance) return cachedInstance;
123
149
 
124
150
  if (!initPromise) {
@@ -1,91 +1,175 @@
1
- /**
2
- * Server-Side Session Decoder
3
- *
4
- * Reads the JWT session cookie, decodes it with jose, and fetches the
5
- * full session from Redis. Used by authGuard (layouts) and withAuth (API routes).
6
- *
7
- * Zero HTTP self-fetches. Direct Redis reads only.
8
- */
9
-
10
- import 'server-only';
11
- import { cookies } from 'next/headers';
12
- import { jwtVerify, type JWTPayload } from 'jose';
13
- import { getSession, type SessionData } from '../lib/session-store';
14
- import { getIDPClientConfig } from '../lib/idp-client-config';
15
- import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
16
- import { ensureInitialized } from '../lib/startup-init';
17
-
18
- export interface DecodedSession {
19
- sessionData: SessionData;
20
- jwtPayload: JWTPayload & { sessionToken?: string; redisSessionId?: string };
21
- }
22
-
23
- /**
24
- * Decode the session from cookies and Redis.
25
- * Returns null if no valid session exists.
26
- *
27
- * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
28
- * If omitted, uses next/headers cookies() for server components.
29
- */
30
- export async function decodeSession(
31
- requestCookies?: { get: (name: string) => { value: string } | undefined }
32
- ): Promise<DecodedSession | null> {
33
- try {
34
- // Ensure startup initialization is complete (Redis, IDP config, etc.)
35
- await ensureInitialized();
36
-
37
- // Get the JWT cookie value
38
- const cookieStore = requestCookies || (await cookies());
39
- const sessionCookieName = getSessionCookieName();
40
- const secureCookieName = getSecureSessionCookieName();
41
-
42
- const cookieValue =
43
- cookieStore.get(secureCookieName)?.value ||
44
- cookieStore.get(sessionCookieName)?.value;
45
-
46
- if (!cookieValue) {
47
- return null;
48
- }
49
-
50
- // Get the NextAuth secret from IDP config
51
- const config = await getIDPClientConfig();
52
- const secret = config.nextAuthSecret;
53
- if (!secret) {
54
- console.error('[DECODE-SESSION] No nextAuthSecret available from IDP config');
55
- return null;
56
- }
57
-
58
- // Decode the JWT (same pattern as test-aware-get-token.ts)
59
- const secretKey = new TextEncoder().encode(secret);
60
- let payload: JWTPayload;
61
- try {
62
- const result = await jwtVerify(cookieValue, secretKey);
63
- payload = result.payload;
64
- } catch (jwtError) {
65
- // JWT decode failed - cookie may be corrupted or secret rotated
66
- console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
67
- return null;
68
- }
69
-
70
- // Extract the Redis session ID from JWT payload
71
- const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
72
- if (!sessionToken) {
73
- console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
74
- return null;
75
- }
76
-
77
- // Fetch session from Redis (direct, no HTTP)
78
- const sessionData = await getSession(sessionToken);
79
- if (!sessionData) {
80
- return null;
81
- }
82
-
83
- return {
84
- sessionData,
85
- jwtPayload: payload as DecodedSession['jwtPayload'],
86
- };
87
- } catch (error) {
88
- console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
89
- return null;
90
- }
91
- }
1
+ /**
2
+ * Server-Side Session Decoder
3
+ *
4
+ * Uses Better Auth's server-side session API to get the current session.
5
+ * Falls back to legacy JWT + Redis path if Better Auth session not found.
6
+ */
7
+
8
+ import 'server-only';
9
+ import { cookies } from 'next/headers';
10
+ import { jwtVerify, type JWTPayload } from 'jose';
11
+ import { getSession, type SessionData } from '../lib/session-store';
12
+ import { getIDPClientConfig } from '../lib/idp-client-config';
13
+ import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
14
+ import { ensureInitialized } from '../lib/startup-init';
15
+
16
+ export interface DecodedSession {
17
+ sessionData: SessionData;
18
+ jwtPayload: JWTPayload & { sessionToken?: string; redisSessionId?: string };
19
+ }
20
+
21
+ /**
22
+ * Try Better Auth's server-side session API.
23
+ * Returns a DecodedSession if Better Auth has an active session, null otherwise.
24
+ */
25
+ async function tryBetterAuthSession(
26
+ requestCookies?: { get: (name: string) => { value: string } | undefined }
27
+ ): Promise<DecodedSession | null> {
28
+ try {
29
+ const { getBetterAuthHandler } = await import('../auth/better-auth');
30
+ // getBetterAuthHandler initializes the instance; we need the raw instance
31
+ const { default: getBetterAuthInstanceFn } = await import('../auth/better-auth')
32
+ .then(m => ({ default: (m as any).getBetterAuthInstance || null }))
33
+ .catch(() => ({ default: null }));
34
+
35
+ // Access the cached instance via the module's internal getter
36
+ let auth: any = null;
37
+ try {
38
+ // Force handler init which caches the instance, then use the API
39
+ await getBetterAuthHandler();
40
+ // The instance is cached in the module — re-import to access it
41
+ const mod = await import('../auth/better-auth');
42
+ auth = (mod as any).__betterAuthInstance;
43
+ } catch {
44
+ return null;
45
+ }
46
+
47
+ if (!auth?.api?.getSession) {
48
+ return null;
49
+ }
50
+
51
+ // Build headers from cookies for Better Auth to read
52
+ const cookieStore = requestCookies || (await cookies());
53
+ const headerObj = new Headers();
54
+
55
+ // Collect all cookies into a Cookie header
56
+ if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
57
+ const allCookies = (cookieStore as any).getAll();
58
+ const cookieStr = allCookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
59
+ headerObj.set('cookie', cookieStr);
60
+ } else {
61
+ // Fallback: read known cookie names
62
+ const sessionCookieName = getSessionCookieName();
63
+ const secureCookieName = getSecureSessionCookieName();
64
+ const parts: string[] = [];
65
+ const sc = cookieStore.get(secureCookieName);
66
+ if (sc?.value) parts.push(`${secureCookieName}=${sc.value}`);
67
+ const nc = cookieStore.get(sessionCookieName);
68
+ if (nc?.value) parts.push(`${sessionCookieName}=${nc.value}`);
69
+ if (parts.length > 0) headerObj.set('cookie', parts.join('; '));
70
+ }
71
+
72
+ const result = await auth.api.getSession({ headers: headerObj });
73
+
74
+ if (!result?.session || !result?.user) {
75
+ return null;
76
+ }
77
+
78
+ // Map Better Auth session to SessionData
79
+ const sessionData: SessionData = {
80
+ userId: result.user.id || '',
81
+ email: result.user.email || '',
82
+ name: result.user.name || undefined,
83
+ roles: [],
84
+ idpAccessTokenExpires: result.session.expiresAt
85
+ ? new Date(result.session.expiresAt).getTime()
86
+ : Date.now() + 24 * 60 * 60 * 1000,
87
+ mfaVerified: true, // Social login doesn't require MFA
88
+ oauthProvider: 'google',
89
+ };
90
+
91
+ const jwtPayload: DecodedSession['jwtPayload'] = {
92
+ sub: result.user.id,
93
+ email: result.user.email,
94
+ name: result.user.name,
95
+ iat: Math.floor(Date.now() / 1000),
96
+ exp: sessionData.idpAccessTokenExpires / 1000,
97
+ sessionToken: result.session.token,
98
+ };
99
+
100
+ return { sessionData, jwtPayload };
101
+ } catch (error) {
102
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Decode the session from cookies.
109
+ * Tries Better Auth first, falls back to legacy JWT + Redis.
110
+ *
111
+ * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
112
+ * If omitted, uses next/headers cookies() for server components.
113
+ */
114
+ export async function decodeSession(
115
+ requestCookies?: { get: (name: string) => { value: string } | undefined }
116
+ ): Promise<DecodedSession | null> {
117
+ try {
118
+ await ensureInitialized();
119
+
120
+ // Try Better Auth session first
121
+ const betterAuthSession = await tryBetterAuthSession(requestCookies);
122
+ if (betterAuthSession) {
123
+ return betterAuthSession;
124
+ }
125
+
126
+ // Fall back to legacy JWT + Redis path
127
+ const cookieStore = requestCookies || (await cookies());
128
+ const sessionCookieName = getSessionCookieName();
129
+ const secureCookieName = getSecureSessionCookieName();
130
+
131
+ const cookieValue =
132
+ cookieStore.get(secureCookieName)?.value ||
133
+ cookieStore.get(sessionCookieName)?.value;
134
+
135
+ if (!cookieValue) {
136
+ return null;
137
+ }
138
+
139
+ const config = await getIDPClientConfig();
140
+ const secret = config.nextAuthSecret;
141
+ if (!secret) {
142
+ console.error('[DECODE-SESSION] No nextAuthSecret available from IDP config');
143
+ return null;
144
+ }
145
+
146
+ const secretKey = new TextEncoder().encode(secret);
147
+ let payload: JWTPayload;
148
+ try {
149
+ const result = await jwtVerify(cookieValue, secretKey);
150
+ payload = result.payload;
151
+ } catch (jwtError) {
152
+ console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
153
+ return null;
154
+ }
155
+
156
+ const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
157
+ if (!sessionToken) {
158
+ console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
159
+ return null;
160
+ }
161
+
162
+ const sessionData = await getSession(sessionToken);
163
+ if (!sessionData) {
164
+ return null;
165
+ }
166
+
167
+ return {
168
+ sessionData,
169
+ jwtPayload: payload as DecodedSession['jwtPayload'],
170
+ };
171
+ } catch (error) {
172
+ console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
173
+ return null;
174
+ }
175
+ }