@payez/next-mvp 4.1.1 → 4.1.2
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.
- package/dist/auth/better-auth.d.ts +3 -0
- package/dist/auth/better-auth.js +22 -1
- package/dist/lib/ensure-fresh-access-token.d.ts +30 -0
- package/dist/lib/ensure-fresh-access-token.js +269 -0
- package/dist/lib/session-store.js +24 -21
- package/dist/lib/token-lifecycle.js +2 -0
- package/dist/models/SessionModel.d.ts +3 -0
- package/dist/models/SessionModel.js +3 -0
- package/dist/routes/auth/session.js +1 -1
- package/dist/server/auth.d.ts +59 -0
- package/dist/server/auth.js +156 -16
- package/dist/server/decode-session.js +2 -0
- package/package.json +6 -1
- package/src/auth/better-auth.ts +434 -408
- package/src/lib/ensure-fresh-access-token.ts +320 -0
- package/src/lib/session-store.ts +692 -689
- package/src/lib/token-lifecycle.ts +470 -468
- package/src/models/SessionModel.ts +264 -258
- package/src/routes/auth/session.ts +166 -166
- package/src/server/auth.ts +272 -78
- package/src/server/decode-session.ts +202 -200
|
@@ -1,200 +1,202 @@
|
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId,
|
|
95
|
+
merchantId: idpTokens?.merchantId,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Backwards compat: session.user.email works alongside session.email
|
|
99
|
+
(sessionData as any).user = {
|
|
100
|
+
id: sessionData.userId,
|
|
101
|
+
email: sessionData.email,
|
|
102
|
+
name: sessionData.name,
|
|
103
|
+
roles: sessionData.roles,
|
|
104
|
+
image: result.user.image,
|
|
105
|
+
oauthProvider: sessionData.oauthProvider,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const jwtPayload: DecodedSession['jwtPayload'] = {
|
|
109
|
+
sub: result.user.id,
|
|
110
|
+
email: result.user.email,
|
|
111
|
+
name: result.user.name,
|
|
112
|
+
iat: Math.floor(Date.now() / 1000),
|
|
113
|
+
exp: sessionData.idpAccessTokenExpires / 1000,
|
|
114
|
+
sessionToken: result.session.token,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return { sessionData, jwtPayload };
|
|
118
|
+
} catch (error) {
|
|
119
|
+
console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Decode the session from cookies.
|
|
126
|
+
* Tries Better Auth first, falls back to legacy JWT + Redis.
|
|
127
|
+
*
|
|
128
|
+
* @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
|
|
129
|
+
* If omitted, uses next/headers cookies() for server components.
|
|
130
|
+
*/
|
|
131
|
+
export async function decodeSession(
|
|
132
|
+
requestCookies?: { get: (name: string) => { value: string } | undefined }
|
|
133
|
+
): Promise<DecodedSession | null> {
|
|
134
|
+
try {
|
|
135
|
+
await ensureInitialized();
|
|
136
|
+
|
|
137
|
+
// Try Better Auth session first
|
|
138
|
+
const betterAuthSession = await tryBetterAuthSession(requestCookies);
|
|
139
|
+
if (betterAuthSession) {
|
|
140
|
+
return betterAuthSession;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Fall back to legacy JWT + Redis path
|
|
144
|
+
const cookieStore = requestCookies || (await cookies());
|
|
145
|
+
const sessionCookieName = getSessionCookieName();
|
|
146
|
+
const secureCookieName = getSecureSessionCookieName();
|
|
147
|
+
|
|
148
|
+
const cookieValue =
|
|
149
|
+
cookieStore.get(secureCookieName)?.value ||
|
|
150
|
+
cookieStore.get(sessionCookieName)?.value;
|
|
151
|
+
|
|
152
|
+
if (!cookieValue) {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const config = await getIDPClientConfig();
|
|
157
|
+
const secret = config.authSecret;
|
|
158
|
+
if (!secret) {
|
|
159
|
+
console.error('[DECODE-SESSION] No authSecret available from IDP config');
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const secretKey = new TextEncoder().encode(secret);
|
|
164
|
+
let payload: JWTPayload;
|
|
165
|
+
try {
|
|
166
|
+
const result = await jwtVerify(cookieValue, secretKey);
|
|
167
|
+
payload = result.payload;
|
|
168
|
+
} catch (jwtError) {
|
|
169
|
+
console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
|
|
174
|
+
if (!sessionToken) {
|
|
175
|
+
console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sessionData = await getSession(sessionToken);
|
|
180
|
+
if (!sessionData) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Backwards compat: session.user.email works alongside session.email
|
|
185
|
+
if (!(sessionData as any).user) {
|
|
186
|
+
(sessionData as any).user = {
|
|
187
|
+
id: sessionData.userId,
|
|
188
|
+
email: sessionData.email,
|
|
189
|
+
name: sessionData.name,
|
|
190
|
+
roles: sessionData.roles,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
sessionData,
|
|
196
|
+
jwtPayload: payload as DecodedSession['jwtPayload'],
|
|
197
|
+
};
|
|
198
|
+
} catch (error) {
|
|
199
|
+
console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|