@payez/next-mvp 4.0.49 → 4.1.0

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.
@@ -1,188 +1,78 @@
1
- /**
2
- * Server-side auth utilities for Better Auth.
3
- *
4
- * All server-side auth flows go through the Better Auth instance returned by
5
- * getAuthInstance(); use getSession(req) for the request-scoped session.
6
- */
7
-
8
- import 'server-only';
9
- import { createBetterAuthInstance } from '../auth/better-auth';
10
- import { getIDPClientConfig } from '../lib/idp-client-config';
11
- import {
12
- getSession as getRedisSession,
13
- getBetterAuthSession as getBetterAuthRedisSession,
14
- type SessionData,
15
- } from '../lib/session-store';
16
-
17
- let authInstance: ReturnType<typeof createBetterAuthInstance> | null = null;
18
- let authInitPromise: Promise<ReturnType<typeof createBetterAuthInstance>> | null = null;
19
-
20
- export type IdpTokenResult =
21
- | { success: true; accessToken: string; sessionData: SessionData }
22
- | { success: false; error: 'NO_SESSION' | 'NO_TOKEN'; terminal: true };
23
-
24
- function buildSessionDataFromAuthSession(session: any): SessionData | null {
25
- const user = session?.user;
26
- if (!user?.id && !user?.email) {
27
- return null;
28
- }
29
-
30
- const expiresAt = session?.session?.expiresAt
31
- ? new Date(session.session.expiresAt).getTime()
32
- : Date.now() + 24 * 60 * 60 * 1000;
33
-
34
- return {
35
- userId: user.userId || user.id || '',
36
- email: user.email || '',
37
- name: user.name || undefined,
38
- roles: Array.isArray(user.roles) ? user.roles : [],
39
- idpAccessToken: user.idpAccessToken,
40
- idpRefreshToken: user.idpRefreshToken,
41
- idpAccessTokenExpires: user.idpAccessTokenExpires || expiresAt,
42
- mfaVerified: user.mfaVerified ?? user.twoFactorSessionVerified ?? false,
43
- oauthProvider: user.oauthProvider,
44
- idpClientId: user.idpClientId,
45
- merchantId: user.merchantId,
46
- };
47
- }
48
-
49
- function attachSessionData(session: any, sessionData: SessionData | null, sessionToken?: string) {
50
- if (!sessionData) {
51
- return session;
52
- }
53
-
54
- const enrichedSessionData = {
55
- ...sessionData,
56
- ...(sessionToken ? { sessionToken } : {}),
57
- };
58
-
59
- (session as any).sessionData = enrichedSessionData;
60
-
61
- if (session?.user) {
62
- const user = session.user as any;
63
- user.userId = enrichedSessionData.userId || user.userId;
64
- user.email = enrichedSessionData.email || user.email;
65
- user.name = enrichedSessionData.name || user.name;
66
- user.roles = enrichedSessionData.roles || user.roles || [];
67
- user.idpAccessToken = enrichedSessionData.idpAccessToken;
68
- user.idpRefreshToken = enrichedSessionData.idpRefreshToken;
69
- user.idpAccessTokenExpires = enrichedSessionData.idpAccessTokenExpires;
70
- user.mfaVerified = enrichedSessionData.mfaVerified;
71
- user.twoFactorSessionVerified =
72
- enrichedSessionData.mfaVerified ?? user.twoFactorSessionVerified;
73
- user.oauthProvider = enrichedSessionData.oauthProvider || user.oauthProvider;
74
- user.idpClientId = enrichedSessionData.idpClientId || user.idpClientId;
75
- user.merchantId = enrichedSessionData.merchantId || user.merchantId;
76
- }
77
-
78
- return session;
79
- }
80
-
81
- /**
82
- * Get the initialized Better Auth instance (singleton).
83
- */
84
- export async function getAuthInstance() {
85
- if (authInstance) return authInstance;
86
- if (!authInitPromise) {
87
- authInitPromise = getIDPClientConfig(true).then(config => {
88
- authInstance = createBetterAuthInstance(config);
89
- return authInstance;
90
- });
91
- }
92
- return authInitPromise;
93
- }
94
-
95
- /**
96
- * Get the current session from a request.
97
- * Replaces getToken() and getServerSession().
98
- *
99
- * Returns the session object or null if not authenticated.
100
- */
101
- export async function getSession(request?: Request): Promise<any> {
102
- const auth = await getAuthInstance();
103
- if (!request) return null;
104
-
105
- try {
106
- const session = await auth.api.getSession({ headers: request.headers });
107
- if (!session?.session?.token || !session?.user) return session;
108
-
109
- const sessionToken = session.session.token as string;
110
- let sessionData: SessionData | null = null;
111
-
112
- // Prefer the app's normalized Redis session. Fall back to Better Auth's
113
- // secondary storage record, then finally to whatever Better Auth already
114
- // put on the request session object.
115
- try {
116
- sessionData = await getRedisSession(sessionToken);
117
- if (!sessionData) {
118
- sessionData = await getBetterAuthRedisSession(sessionToken);
119
- }
120
- } catch { /* Redis unavailable */ }
121
-
122
- if (!sessionData) {
123
- sessionData = buildSessionDataFromAuthSession(session);
124
- }
125
-
126
- return attachSessionData(session, sessionData, sessionToken);
127
- } catch {
128
- return null;
129
- }
130
- }
131
-
132
- /**
133
- * Get normalized session data for the current request.
134
- *
135
- * This prefers the app's Redis session because it carries the canonical
136
- * IDP token, roles, and tenant-specific user identity used by app routes.
137
- */
138
- export async function getSessionData(request?: Request): Promise<SessionData | null> {
139
- const session = await getSession(request);
140
- const sessionData =
141
- ((session as any)?.sessionData as SessionData | undefined) ||
142
- buildSessionDataFromAuthSession(session);
143
-
144
- if (!sessionData) {
145
- return null;
146
- }
147
-
148
- const sessionToken = session?.session?.token as string | undefined;
149
- return sessionToken
150
- ? { ...sessionData, sessionToken }
151
- : sessionData;
152
- }
153
-
154
- /**
155
- * Get the current request's IDP access token without triggering a refresh.
156
- *
157
- * Use this for routes that only need the currently-issued bearer token and
158
- * should fail closed instead of performing token lifecycle work.
159
- */
160
- export async function getIdpToken(request?: Request): Promise<IdpTokenResult> {
161
- const sessionData = await getSessionData(request);
162
- if (!sessionData) {
163
- return { success: false, error: 'NO_SESSION', terminal: true };
164
- }
165
-
166
- const accessToken = sessionData.idpAccessToken || (sessionData as any).accessToken;
167
- if (!accessToken) {
168
- return { success: false, error: 'NO_TOKEN', terminal: true };
169
- }
170
-
171
- return {
172
- success: true,
173
- accessToken,
174
- sessionData,
175
- };
176
- }
177
-
178
- /**
179
- * Get the current session, throwing if not authenticated.
180
- * Use in API handlers that require auth.
181
- */
182
- export async function requireSession(request: Request) {
183
- const session = await getSession(request);
184
- if (!session) {
185
- throw new Error('Unauthorized');
186
- }
187
- return session;
188
- }
1
+ /**
2
+ * Server-side auth utilities for Better Auth.
3
+ *
4
+ * All server-side auth flows go through the Better Auth instance returned by
5
+ * getAuthInstance(); use getSession(req) for the request-scoped session.
6
+ */
7
+
8
+ import 'server-only';
9
+ import { createBetterAuthInstance } from '../auth/better-auth';
10
+ import { getIDPClientConfig } from '../lib/idp-client-config';
11
+
12
+ let authInstance: ReturnType<typeof createBetterAuthInstance> | null = null;
13
+ let authInitPromise: Promise<ReturnType<typeof createBetterAuthInstance>> | null = null;
14
+
15
+ /**
16
+ * Get the initialized Better Auth instance (singleton).
17
+ */
18
+ export async function getAuthInstance() {
19
+ if (authInstance) return authInstance;
20
+ if (!authInitPromise) {
21
+ authInitPromise = getIDPClientConfig(true).then(config => {
22
+ authInstance = createBetterAuthInstance(config);
23
+ return authInstance;
24
+ });
25
+ }
26
+ return authInitPromise;
27
+ }
28
+
29
+ /**
30
+ * Get the current session from a request.
31
+ * Replaces getToken() and getServerSession().
32
+ *
33
+ * Returns the session object or null if not authenticated.
34
+ */
35
+ export async function getSession(request?: Request): Promise<any> {
36
+ const auth = await getAuthInstance();
37
+ if (!request) return null;
38
+
39
+ try {
40
+ const session = await auth.api.getSession({ headers: request.headers });
41
+ if (!session?.session?.token || !session?.user) return session;
42
+
43
+ // Enrich with IDP tokens from Redis (stored by post-login hook)
44
+ try {
45
+ const { getRedis } = await import('../lib/redis');
46
+ const { getAppSlug } = await import('../lib/app-slug');
47
+ const baKey = `ba:${getAppSlug()}:${session.session.token}`;
48
+ const baRaw = await getRedis().get(baKey);
49
+ if (baRaw) {
50
+ const baData = JSON.parse(baRaw);
51
+ if (baData.idpTokens) {
52
+ const u = session.user as any;
53
+ u.roles = baData.idpTokens.roles || [];
54
+ u.userId = baData.idpTokens.userId;
55
+ u.idpAccessToken = baData.idpTokens.idpAccessToken;
56
+ u.idpRefreshToken = baData.idpTokens.idpRefreshToken;
57
+ u.idpAccessTokenExpires = baData.idpTokens.idpAccessTokenExpires;
58
+ }
59
+ }
60
+ } catch { /* Redis unavailable */ }
61
+
62
+ return session;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Get the current session, throwing if not authenticated.
70
+ * Use in API handlers that require auth.
71
+ */
72
+ export async function requireSession(request: Request) {
73
+ const session = await getSession(request);
74
+ if (!session) {
75
+ throw new Error('Unauthorized');
76
+ }
77
+ return session;
78
+ }
@@ -1,200 +1,200 @@
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 { getBetterAuthInstance } = await import('../auth/better-auth');
30
-
31
- let auth: any = null;
32
- try {
33
- auth = await getBetterAuthInstance();
34
- } catch {
35
- return null;
36
- }
37
-
38
- if (!auth?.api?.getSession) {
39
- return null;
40
- }
41
-
42
- // Build headers from cookies for Better Auth to read
43
- const cookieStore = requestCookies || (await cookies());
44
- const headerObj = new Headers();
45
-
46
- // Collect all cookies into a Cookie header
47
- if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
48
- const allCookies = (cookieStore as any).getAll();
49
- const cookieStr = allCookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
50
- headerObj.set('cookie', cookieStr);
51
- } else {
52
- // Fallback: read known cookie names
53
- const sessionCookieName = getSessionCookieName();
54
- const secureCookieName = getSecureSessionCookieName();
55
- const parts: string[] = [];
56
- const sc = cookieStore.get(secureCookieName);
57
- if (sc?.value) parts.push(`${secureCookieName}=${sc.value}`);
58
- const nc = cookieStore.get(sessionCookieName);
59
- if (nc?.value) parts.push(`${sessionCookieName}=${nc.value}`);
60
- if (parts.length > 0) headerObj.set('cookie', parts.join('; '));
61
- }
62
-
63
- const result = await auth.api.getSession({ headers: headerObj });
64
-
65
- if (!result?.session || !result?.user) {
66
- return null;
67
- }
68
-
69
- // Read IDP tokens from BA Redis session (stored by callback route after OAuth)
70
- let idpTokens: any = null;
71
- try {
72
- const { getRedis } = await import('../lib/redis');
73
- const { getAppSlug } = await import('../lib/app-slug');
74
- const baKey = `ba:${getAppSlug()}:${result.session.token}`;
75
- const baRaw = await getRedis().get(baKey);
76
- if (baRaw) {
77
- const baData = JSON.parse(baRaw);
78
- idpTokens = baData.idpTokens;
79
- }
80
- } catch { /* Redis unavailable */ }
81
-
82
- // Map Better Auth session + IDP tokens to SessionData
83
- const sessionData: SessionData = {
84
- userId: idpTokens?.userId || result.user.id || '',
85
- email: idpTokens?.email || result.user.email || '',
86
- name: idpTokens?.name || result.user.name || undefined,
87
- roles: idpTokens?.roles || [],
88
- idpAccessToken: idpTokens?.idpAccessToken,
89
- idpRefreshToken: idpTokens?.idpRefreshToken,
90
- idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
91
- || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
92
- mfaVerified: idpTokens?.mfaVerified ?? false,
93
- oauthProvider: 'google',
94
- };
95
-
96
- // Backwards compat: session.user.email works alongside session.email
97
- (sessionData as any).user = {
98
- id: sessionData.userId,
99
- email: sessionData.email,
100
- name: sessionData.name,
101
- roles: sessionData.roles,
102
- image: result.user.image,
103
- oauthProvider: sessionData.oauthProvider,
104
- };
105
-
106
- const jwtPayload: DecodedSession['jwtPayload'] = {
107
- sub: result.user.id,
108
- email: result.user.email,
109
- name: result.user.name,
110
- iat: Math.floor(Date.now() / 1000),
111
- exp: sessionData.idpAccessTokenExpires / 1000,
112
- sessionToken: result.session.token,
113
- };
114
-
115
- return { sessionData, jwtPayload };
116
- } catch (error) {
117
- console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
118
- return null;
119
- }
120
- }
121
-
122
- /**
123
- * Decode the session from cookies.
124
- * Tries Better Auth first, falls back to legacy JWT + Redis.
125
- *
126
- * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
127
- * If omitted, uses next/headers cookies() for server components.
128
- */
129
- export async function decodeSession(
130
- requestCookies?: { get: (name: string) => { value: string } | undefined }
131
- ): Promise<DecodedSession | null> {
132
- try {
133
- await ensureInitialized();
134
-
135
- // Try Better Auth session first
136
- const betterAuthSession = await tryBetterAuthSession(requestCookies);
137
- if (betterAuthSession) {
138
- return betterAuthSession;
139
- }
140
-
141
- // Fall back to legacy JWT + Redis path
142
- const cookieStore = requestCookies || (await cookies());
143
- const sessionCookieName = getSessionCookieName();
144
- const secureCookieName = getSecureSessionCookieName();
145
-
146
- const cookieValue =
147
- cookieStore.get(secureCookieName)?.value ||
148
- cookieStore.get(sessionCookieName)?.value;
149
-
150
- if (!cookieValue) {
151
- return null;
152
- }
153
-
154
- const config = await getIDPClientConfig();
155
- const secret = config.authSecret;
156
- if (!secret) {
157
- console.error('[DECODE-SESSION] No authSecret available from IDP config');
158
- return null;
159
- }
160
-
161
- const secretKey = new TextEncoder().encode(secret);
162
- let payload: JWTPayload;
163
- try {
164
- const result = await jwtVerify(cookieValue, secretKey);
165
- payload = result.payload;
166
- } catch (jwtError) {
167
- console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
168
- return null;
169
- }
170
-
171
- const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
172
- if (!sessionToken) {
173
- console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
174
- return null;
175
- }
176
-
177
- const sessionData = await getSession(sessionToken);
178
- if (!sessionData) {
179
- return null;
180
- }
181
-
182
- // Backwards compat: session.user.email works alongside session.email
183
- if (!(sessionData as any).user) {
184
- (sessionData as any).user = {
185
- id: sessionData.userId,
186
- email: sessionData.email,
187
- name: sessionData.name,
188
- roles: sessionData.roles,
189
- };
190
- }
191
-
192
- return {
193
- sessionData,
194
- jwtPayload: payload as DecodedSession['jwtPayload'],
195
- };
196
- } catch (error) {
197
- console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
198
- return null;
199
- }
200
- }
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 { getBetterAuthInstance } = await import('../auth/better-auth');
30
+
31
+ let auth: any = null;
32
+ try {
33
+ auth = await getBetterAuthInstance();
34
+ } catch {
35
+ return null;
36
+ }
37
+
38
+ if (!auth?.api?.getSession) {
39
+ return null;
40
+ }
41
+
42
+ // Build headers from cookies for Better Auth to read
43
+ const cookieStore = requestCookies || (await cookies());
44
+ const headerObj = new Headers();
45
+
46
+ // Collect all cookies into a Cookie header
47
+ if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
48
+ const allCookies = (cookieStore as any).getAll();
49
+ const cookieStr = allCookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
50
+ headerObj.set('cookie', cookieStr);
51
+ } else {
52
+ // Fallback: read known cookie names
53
+ const sessionCookieName = getSessionCookieName();
54
+ const secureCookieName = getSecureSessionCookieName();
55
+ const parts: string[] = [];
56
+ const sc = cookieStore.get(secureCookieName);
57
+ if (sc?.value) parts.push(`${secureCookieName}=${sc.value}`);
58
+ const nc = cookieStore.get(sessionCookieName);
59
+ if (nc?.value) parts.push(`${sessionCookieName}=${nc.value}`);
60
+ if (parts.length > 0) headerObj.set('cookie', parts.join('; '));
61
+ }
62
+
63
+ const result = await auth.api.getSession({ headers: headerObj });
64
+
65
+ if (!result?.session || !result?.user) {
66
+ return null;
67
+ }
68
+
69
+ // Read IDP tokens from BA Redis session (stored by callback route after OAuth)
70
+ let idpTokens: any = null;
71
+ try {
72
+ const { getRedis } = await import('../lib/redis');
73
+ const { getAppSlug } = await import('../lib/app-slug');
74
+ const baKey = `ba:${getAppSlug()}:${result.session.token}`;
75
+ const baRaw = await getRedis().get(baKey);
76
+ if (baRaw) {
77
+ const baData = JSON.parse(baRaw);
78
+ idpTokens = baData.idpTokens;
79
+ }
80
+ } catch { /* Redis unavailable */ }
81
+
82
+ // Map Better Auth session + IDP tokens to SessionData
83
+ const sessionData: SessionData = {
84
+ userId: idpTokens?.userId || result.user.id || '',
85
+ email: idpTokens?.email || result.user.email || '',
86
+ name: idpTokens?.name || result.user.name || undefined,
87
+ roles: idpTokens?.roles || [],
88
+ idpAccessToken: idpTokens?.idpAccessToken,
89
+ idpRefreshToken: idpTokens?.idpRefreshToken,
90
+ idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
91
+ || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
92
+ mfaVerified: idpTokens?.mfaVerified ?? false,
93
+ oauthProvider: 'google',
94
+ };
95
+
96
+ // Backwards compat: session.user.email works alongside session.email
97
+ (sessionData as any).user = {
98
+ id: sessionData.userId,
99
+ email: sessionData.email,
100
+ name: sessionData.name,
101
+ roles: sessionData.roles,
102
+ image: result.user.image,
103
+ oauthProvider: sessionData.oauthProvider,
104
+ };
105
+
106
+ const jwtPayload: DecodedSession['jwtPayload'] = {
107
+ sub: result.user.id,
108
+ email: result.user.email,
109
+ name: result.user.name,
110
+ iat: Math.floor(Date.now() / 1000),
111
+ exp: sessionData.idpAccessTokenExpires / 1000,
112
+ sessionToken: result.session.token,
113
+ };
114
+
115
+ return { sessionData, jwtPayload };
116
+ } catch (error) {
117
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
118
+ return null;
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Decode the session from cookies.
124
+ * Tries Better Auth first, falls back to legacy JWT + Redis.
125
+ *
126
+ * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
127
+ * If omitted, uses next/headers cookies() for server components.
128
+ */
129
+ export async function decodeSession(
130
+ requestCookies?: { get: (name: string) => { value: string } | undefined }
131
+ ): Promise<DecodedSession | null> {
132
+ try {
133
+ await ensureInitialized();
134
+
135
+ // Try Better Auth session first
136
+ const betterAuthSession = await tryBetterAuthSession(requestCookies);
137
+ if (betterAuthSession) {
138
+ return betterAuthSession;
139
+ }
140
+
141
+ // Fall back to legacy JWT + Redis path
142
+ const cookieStore = requestCookies || (await cookies());
143
+ const sessionCookieName = getSessionCookieName();
144
+ const secureCookieName = getSecureSessionCookieName();
145
+
146
+ const cookieValue =
147
+ cookieStore.get(secureCookieName)?.value ||
148
+ cookieStore.get(sessionCookieName)?.value;
149
+
150
+ if (!cookieValue) {
151
+ return null;
152
+ }
153
+
154
+ const config = await getIDPClientConfig();
155
+ const secret = config.authSecret;
156
+ if (!secret) {
157
+ console.error('[DECODE-SESSION] No authSecret available from IDP config');
158
+ return null;
159
+ }
160
+
161
+ const secretKey = new TextEncoder().encode(secret);
162
+ let payload: JWTPayload;
163
+ try {
164
+ const result = await jwtVerify(cookieValue, secretKey);
165
+ payload = result.payload;
166
+ } catch (jwtError) {
167
+ console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
168
+ return null;
169
+ }
170
+
171
+ const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
172
+ if (!sessionToken) {
173
+ console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
174
+ return null;
175
+ }
176
+
177
+ const sessionData = await getSession(sessionToken);
178
+ if (!sessionData) {
179
+ return null;
180
+ }
181
+
182
+ // Backwards compat: session.user.email works alongside session.email
183
+ if (!(sessionData as any).user) {
184
+ (sessionData as any).user = {
185
+ id: sessionData.userId,
186
+ email: sessionData.email,
187
+ name: sessionData.name,
188
+ roles: sessionData.roles,
189
+ };
190
+ }
191
+
192
+ return {
193
+ sessionData,
194
+ jwtPayload: payload as DecodedSession['jwtPayload'],
195
+ };
196
+ } catch (error) {
197
+ console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
198
+ return null;
199
+ }
200
+ }