@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.
- package/dist/auth/better-auth.d.ts +12 -1
- package/dist/auth/better-auth.js +34 -4
- package/dist/server/auth.d.ts +5 -1
- package/dist/server/decode-session.d.ts +4 -6
- package/dist/server/decode-session.js +122 -13
- package/package.json +1 -1
- package/src/auth/better-auth.ts +30 -4
- package/src/server/decode-session.ts +175 -91
|
@@ -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
|
*
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -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
|
-
//
|
|
65
|
-
|
|
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
|
});
|
package/dist/server/auth.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
5
|
-
*
|
|
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
|
|
21
|
-
*
|
|
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
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
21
|
-
* Returns
|
|
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
|
-
//
|
|
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
package/src/auth/better-auth.ts
CHANGED
|
@@ -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
|
-
//
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import '
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|