@payez/next-mvp 4.0.5 → 4.0.7

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.
@@ -29,6 +29,7 @@ export declare function buildBetterAuthProviders(config: IDPClientConfig): Recor
29
29
  * Call after getIDPClientConfig() resolves.
30
30
  */
31
31
  export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): import("better-auth").Auth<{
32
+ baseURL: string;
32
33
  secret: string;
33
34
  socialProviders: Record<string, BetterAuthSocialProvider>;
34
35
  trustedOrigins: string[];
@@ -65,6 +66,13 @@ export declare function createBetterAuthInstance(idpConfig: IDPClientConfig): im
65
66
  * Better Auth is always enabled (NextAuth removed in 4.0).
66
67
  */
67
68
  export declare function isBetterAuthEnabled(): boolean;
69
+ /**
70
+ * Get Better Auth Next.js route handlers (GET, POST).
71
+ * Initializes Better Auth from IDP config on first call, caches the instance.
72
+ */
73
+ declare let cachedInstance: any;
74
+ export { cachedInstance as __betterAuthInstance };
75
+ export declare function getBetterAuthInstance(): Promise<any>;
68
76
  /**
69
77
  * Get flag-gated auth handler for Next.js route.
70
78
  *
@@ -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");
@@ -45,13 +47,18 @@ function buildBetterAuthProviders(config) {
45
47
  */
46
48
  function createBetterAuthInstance(idpConfig) {
47
49
  const appSlug = idpConfig.clientSlug || (0, app_slug_1.getAppSlug)();
50
+ // Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
51
+ const baseURL = process.env.BETTER_AUTH_URL
52
+ || idpConfig.baseClientUrl
53
+ || `http://localhost:${process.env.PORT || '3000'}`;
48
54
  return (0, better_auth_1.betterAuth)({
55
+ baseURL,
49
56
  secret: idpConfig.nextAuthSecret,
50
57
  socialProviders: buildBetterAuthProviders(idpConfig),
51
58
  // Trust the app's own origin + any configured base URL
52
59
  trustedOrigins: [
53
- ...(idpConfig.baseClientUrl ? [idpConfig.baseClientUrl] : []),
54
- ...(process.env.BETTER_AUTH_URL ? [process.env.BETTER_AUTH_URL] : []),
60
+ baseURL,
61
+ ...(idpConfig.baseClientUrl && idpConfig.baseClientUrl !== baseURL ? [idpConfig.baseClientUrl] : []),
55
62
  'http://localhost:3000',
56
63
  'http://localhost:3400',
57
64
  'http://localhost:3600',
@@ -91,6 +98,7 @@ function isBetterAuthEnabled() {
91
98
  */
92
99
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
100
  let cachedInstance = null;
101
+ exports.__betterAuthInstance = cachedInstance;
94
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
103
  let initPromise = null;
96
104
  async function getBetterAuthInstance() {
@@ -99,7 +107,7 @@ async function getBetterAuthInstance() {
99
107
  if (!initPromise) {
100
108
  initPromise = (0, idp_client_config_1.getIDPClientConfig)().then(config => {
101
109
  const instance = createBetterAuthInstance(config);
102
- cachedInstance = instance;
110
+ exports.__betterAuthInstance = cachedInstance = instance;
103
111
  console.log('[BETTER_AUTH] Instance created for', config.clientSlug || config.clientId);
104
112
  return instance;
105
113
  });
@@ -12,6 +12,7 @@ import 'server-only';
12
12
  * Get the initialized Better Auth instance (singleton).
13
13
  */
14
14
  export declare function getAuthInstance(): Promise<import("better-auth/types").Auth<{
15
+ baseURL: string;
15
16
  secret: string;
16
17
  socialProviders: Record<string, import("../auth/better-auth").BetterAuthSocialProvider>;
17
18
  trustedOrigins: string[];
@@ -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.5",
3
+ "version": "4.0.7",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -56,15 +56,21 @@ export function buildBetterAuthProviders(
56
56
  export function createBetterAuthInstance(idpConfig: IDPClientConfig) {
57
57
  const appSlug = idpConfig.clientSlug || getAppSlug();
58
58
 
59
+ // Resolve base URL: BETTER_AUTH_URL env > IDP config > localhost fallback
60
+ const baseURL = process.env.BETTER_AUTH_URL
61
+ || idpConfig.baseClientUrl
62
+ || `http://localhost:${process.env.PORT || '3000'}`;
63
+
59
64
  return betterAuth({
65
+ baseURL,
60
66
  secret: idpConfig.nextAuthSecret as string,
61
67
 
62
68
  socialProviders: buildBetterAuthProviders(idpConfig),
63
69
 
64
70
  // Trust the app's own origin + any configured base URL
65
71
  trustedOrigins: [
66
- ...(idpConfig.baseClientUrl ? [idpConfig.baseClientUrl] : []),
67
- ...(process.env.BETTER_AUTH_URL ? [process.env.BETTER_AUTH_URL] : []),
72
+ baseURL,
73
+ ...(idpConfig.baseClientUrl && idpConfig.baseClientUrl !== baseURL ? [idpConfig.baseClientUrl] : []),
68
74
  'http://localhost:3000',
69
75
  'http://localhost:3400',
70
76
  'http://localhost:3600',
@@ -112,7 +118,11 @@ let cachedInstance: any = null;
112
118
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
119
  let initPromise: Promise<any> | null = null;
114
120
 
115
- async function getBetterAuthInstance() {
121
+ // Expose for server-side session access (decode-session.ts)
122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
123
+ export { cachedInstance as __betterAuthInstance };
124
+
125
+ export async function getBetterAuthInstance() {
116
126
  if (cachedInstance) return cachedInstance;
117
127
 
118
128
  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
+ }