@payez/next-mvp 4.1.7 → 4.1.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/lib/idp-fetch.js +25 -9
- package/package.json +1 -1
- package/src/lib/idp-fetch.ts +126 -110
package/dist/lib/idp-fetch.js
CHANGED
|
@@ -67,6 +67,14 @@ async function ensureFreshAccessToken(req) {
|
|
|
67
67
|
return {};
|
|
68
68
|
}
|
|
69
69
|
let session = await (0, session_store_1.getSession)(sessionToken);
|
|
70
|
+
// Fall back to Better Auth Redis key when app session is missing/stale
|
|
71
|
+
if (!session || !session.idpAccessToken) {
|
|
72
|
+
const { getBetterAuthSession } = await Promise.resolve().then(() => __importStar(require('./session-store')));
|
|
73
|
+
const baSession = await getBetterAuthSession(sessionToken);
|
|
74
|
+
if (baSession?.idpAccessToken) {
|
|
75
|
+
session = baSession;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
70
78
|
// Fall back to Better Auth: decode session from cookies to get IDP tokens
|
|
71
79
|
if (!session?.idpAccessToken) {
|
|
72
80
|
try {
|
|
@@ -81,28 +89,34 @@ async function ensureFreshAccessToken(req) {
|
|
|
81
89
|
console.log('[IDP_FETCH] Redis session lookup:', {
|
|
82
90
|
hasSession: !!session,
|
|
83
91
|
hasAccessToken: !!session?.idpAccessToken,
|
|
92
|
+
hasRefreshToken: !!session?.idpRefreshToken,
|
|
84
93
|
bearerKeyId: session?.bearerKeyId || 'NOT_SET',
|
|
85
94
|
idpClientId: session?.idpClientId || 'NOT_SET',
|
|
86
95
|
});
|
|
87
96
|
if (!session?.idpAccessToken)
|
|
88
97
|
return { sessionToken };
|
|
98
|
+
const hasRefreshToken = !!session.idpRefreshToken;
|
|
89
99
|
const now = Date.now();
|
|
90
100
|
const timeLeft = (session.idpAccessTokenExpires ?? now) - now;
|
|
91
101
|
// Treat tokens as effectively expired if within 5 minutes of expiry
|
|
92
102
|
if (timeLeft > 5 * 60 * 1000) {
|
|
93
|
-
return { sessionToken, accessToken: session.idpAccessToken };
|
|
103
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
94
104
|
}
|
|
95
|
-
// attempt refresh
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
// Only attempt refresh when we actually have a refresh token to use.
|
|
106
|
+
// Provisional tokens (issued before 2FA completion) have no refresh token.
|
|
107
|
+
if (hasRefreshToken) {
|
|
108
|
+
try {
|
|
109
|
+
await (0, internal_api_1.internalRefresh)(req.headers.get('cookie') || '', sessionToken);
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
session = await (0, session_store_1.getSession)(sessionToken);
|
|
113
|
+
return { sessionToken, accessToken: session?.idpAccessToken, hasRefreshToken: !!session?.idpRefreshToken };
|
|
98
114
|
}
|
|
99
|
-
|
|
100
|
-
session = await (0, session_store_1.getSession)(sessionToken);
|
|
101
|
-
return { sessionToken, accessToken: session?.idpAccessToken };
|
|
115
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
102
116
|
}
|
|
103
117
|
async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
104
118
|
let attemptedRefresh = false;
|
|
105
|
-
let { accessToken, sessionToken } = await ensureFreshAccessToken(req);
|
|
119
|
+
let { accessToken, sessionToken, hasRefreshToken } = await ensureFreshAccessToken(req);
|
|
106
120
|
const makeCall = async (bearer) => {
|
|
107
121
|
const res = await fetch(targetUrl, {
|
|
108
122
|
...init,
|
|
@@ -119,7 +133,9 @@ async function idpFetchJSON(req, targetUrl, init = {}) {
|
|
|
119
133
|
};
|
|
120
134
|
// First attempt
|
|
121
135
|
let { res, json } = await makeCall(accessToken);
|
|
122
|
-
|
|
136
|
+
// Only attempt refresh on 401/403 if we actually have a refresh token.
|
|
137
|
+
// Provisional tokens (pre-2FA) have no refresh token — refreshing is futile.
|
|
138
|
+
if ((res.status === 401 || res.status === 403) && sessionToken && hasRefreshToken && !attemptedRefresh) {
|
|
123
139
|
attemptedRefresh = true;
|
|
124
140
|
try {
|
|
125
141
|
// Use centralized internal API helper for server-to-server calls
|
package/package.json
CHANGED
package/src/lib/idp-fetch.ts
CHANGED
|
@@ -1,111 +1,127 @@
|
|
|
1
|
-
import { NextRequest } from 'next/server';
|
|
2
|
-
import { getTokenTestAware } from './test-aware-get-token';
|
|
3
|
-
import { getSession } from './session-store';
|
|
4
|
-
import { internalRefresh } from './internal-api';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Centralized IDP fetch helper
|
|
8
|
-
* - Injects Bearer from Redis session
|
|
9
|
-
* - If access token is expired/near-expiry, triggers one refresh and retries fetch once
|
|
10
|
-
* - Returns parsed JSON and HTTP status
|
|
11
|
-
*/
|
|
12
|
-
export interface IdpFetchResult<T = any> {
|
|
13
|
-
ok: boolean;
|
|
14
|
-
status: number;
|
|
15
|
-
json: T | null;
|
|
16
|
-
attemptedRefresh: boolean;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function buildHeaders(req: NextRequest, bearer?: string, extra?: Record<string, string>) {
|
|
20
|
-
const headers: Record<string, string> = {
|
|
21
|
-
Accept: 'application/json',
|
|
22
|
-
'Content-Type': 'application/json',
|
|
23
|
-
...extra,
|
|
24
|
-
};
|
|
25
|
-
const xfwd = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip');
|
|
26
|
-
if (xfwd) headers['X-Forwarded-For'] = xfwd.split(',')[0].trim();
|
|
27
|
-
const ua = req.headers.get('user-agent');
|
|
28
|
-
if (ua) headers['User-Agent'] = ua;
|
|
29
|
-
if (bearer) headers['Authorization'] = `Bearer ${bearer}`;
|
|
30
|
-
return headers;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
async function ensureFreshAccessToken(req: NextRequest): Promise<{ sessionToken?: string; accessToken?: string }> {
|
|
34
|
-
const token = await getTokenTestAware(req);
|
|
35
|
-
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
36
|
-
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
37
|
-
console.log('[IDP_FETCH] ensureFreshAccessToken:', {
|
|
38
|
-
hasToken: !!token,
|
|
39
|
-
sessionToken: sessionToken?.substring(0, 20) + '...',
|
|
40
|
-
pathname: req.nextUrl.pathname
|
|
41
|
-
});
|
|
42
|
-
if (!sessionToken) {
|
|
43
|
-
console.warn('[IDP_FETCH] No sessionToken found in JWT - Bearer token will NOT be sent!');
|
|
44
|
-
return {};
|
|
45
|
-
}
|
|
46
|
-
let session = await getSession(sessionToken);
|
|
47
|
-
// Fall back to Better Auth
|
|
48
|
-
if (!session
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
let
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
|
|
1
|
+
import { NextRequest } from 'next/server';
|
|
2
|
+
import { getTokenTestAware } from './test-aware-get-token';
|
|
3
|
+
import { getSession } from './session-store';
|
|
4
|
+
import { internalRefresh } from './internal-api';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Centralized IDP fetch helper
|
|
8
|
+
* - Injects Bearer from Redis session
|
|
9
|
+
* - If access token is expired/near-expiry, triggers one refresh and retries fetch once
|
|
10
|
+
* - Returns parsed JSON and HTTP status
|
|
11
|
+
*/
|
|
12
|
+
export interface IdpFetchResult<T = any> {
|
|
13
|
+
ok: boolean;
|
|
14
|
+
status: number;
|
|
15
|
+
json: T | null;
|
|
16
|
+
attemptedRefresh: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildHeaders(req: NextRequest, bearer?: string, extra?: Record<string, string>) {
|
|
20
|
+
const headers: Record<string, string> = {
|
|
21
|
+
Accept: 'application/json',
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
...extra,
|
|
24
|
+
};
|
|
25
|
+
const xfwd = req.headers.get('x-forwarded-for') || req.headers.get('x-real-ip');
|
|
26
|
+
if (xfwd) headers['X-Forwarded-For'] = xfwd.split(',')[0].trim();
|
|
27
|
+
const ua = req.headers.get('user-agent');
|
|
28
|
+
if (ua) headers['User-Agent'] = ua;
|
|
29
|
+
if (bearer) headers['Authorization'] = `Bearer ${bearer}`;
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function ensureFreshAccessToken(req: NextRequest): Promise<{ sessionToken?: string; accessToken?: string; hasRefreshToken?: boolean }> {
|
|
34
|
+
const token = await getTokenTestAware(req);
|
|
35
|
+
// Support both field names: sessionToken (auth.ts JWT) and redisSessionId (legacy)
|
|
36
|
+
const sessionToken = (token?.sessionToken || token?.redisSessionId) as string | undefined;
|
|
37
|
+
console.log('[IDP_FETCH] ensureFreshAccessToken:', {
|
|
38
|
+
hasToken: !!token,
|
|
39
|
+
sessionToken: sessionToken?.substring(0, 20) + '...',
|
|
40
|
+
pathname: req.nextUrl.pathname
|
|
41
|
+
});
|
|
42
|
+
if (!sessionToken) {
|
|
43
|
+
console.warn('[IDP_FETCH] No sessionToken found in JWT - Bearer token will NOT be sent!');
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
let session = await getSession(sessionToken);
|
|
47
|
+
// Fall back to Better Auth Redis key when app session is missing/stale
|
|
48
|
+
if (!session || !session.idpAccessToken) {
|
|
49
|
+
const { getBetterAuthSession } = await import('./session-store');
|
|
50
|
+
const baSession = await getBetterAuthSession(sessionToken);
|
|
51
|
+
if (baSession?.idpAccessToken) {
|
|
52
|
+
session = baSession;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Fall back to Better Auth: decode session from cookies to get IDP tokens
|
|
56
|
+
if (!session?.idpAccessToken) {
|
|
57
|
+
try {
|
|
58
|
+
const { decodeSession } = await import('../server/decode-session');
|
|
59
|
+
const decoded = await decodeSession(req.cookies);
|
|
60
|
+
if (decoded?.sessionData?.idpAccessToken) {
|
|
61
|
+
session = decoded.sessionData;
|
|
62
|
+
}
|
|
63
|
+
} catch { /* decodeSession unavailable in edge */ }
|
|
64
|
+
}
|
|
65
|
+
console.log('[IDP_FETCH] Redis session lookup:', {
|
|
66
|
+
hasSession: !!session,
|
|
67
|
+
hasAccessToken: !!session?.idpAccessToken,
|
|
68
|
+
hasRefreshToken: !!session?.idpRefreshToken,
|
|
69
|
+
bearerKeyId: session?.bearerKeyId || 'NOT_SET',
|
|
70
|
+
idpClientId: session?.idpClientId || 'NOT_SET',
|
|
71
|
+
});
|
|
72
|
+
if (!session?.idpAccessToken) return { sessionToken };
|
|
73
|
+
const hasRefreshToken = !!session.idpRefreshToken;
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
const timeLeft = (session.idpAccessTokenExpires ?? now) - now;
|
|
76
|
+
// Treat tokens as effectively expired if within 5 minutes of expiry
|
|
77
|
+
if (timeLeft > 5 * 60 * 1000) {
|
|
78
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
79
|
+
}
|
|
80
|
+
// Only attempt refresh when we actually have a refresh token to use.
|
|
81
|
+
// Provisional tokens (issued before 2FA completion) have no refresh token.
|
|
82
|
+
if (hasRefreshToken) {
|
|
83
|
+
try {
|
|
84
|
+
await internalRefresh(req.headers.get('cookie') || '', sessionToken);
|
|
85
|
+
} catch {}
|
|
86
|
+
session = await getSession(sessionToken);
|
|
87
|
+
return { sessionToken, accessToken: session?.idpAccessToken, hasRefreshToken: !!session?.idpRefreshToken };
|
|
88
|
+
}
|
|
89
|
+
return { sessionToken, accessToken: session.idpAccessToken, hasRefreshToken };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function idpFetchJSON<T = any>(
|
|
93
|
+
req: NextRequest,
|
|
94
|
+
targetUrl: string,
|
|
95
|
+
init: RequestInit = {}
|
|
96
|
+
): Promise<IdpFetchResult<T>> {
|
|
97
|
+
let attemptedRefresh = false;
|
|
98
|
+
let { accessToken, sessionToken, hasRefreshToken } = await ensureFreshAccessToken(req);
|
|
99
|
+
|
|
100
|
+
const makeCall = async (bearer?: string) => {
|
|
101
|
+
const res = await fetch(targetUrl, {
|
|
102
|
+
...init,
|
|
103
|
+
headers: buildHeaders(req, bearer, init.headers as any),
|
|
104
|
+
});
|
|
105
|
+
let json: any = null;
|
|
106
|
+
try { json = await res.json(); } catch { json = null; }
|
|
107
|
+
return { res, json } as const;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// First attempt
|
|
111
|
+
let { res, json } = await makeCall(accessToken);
|
|
112
|
+
// Only attempt refresh on 401/403 if we actually have a refresh token.
|
|
113
|
+
// Provisional tokens (pre-2FA) have no refresh token — refreshing is futile.
|
|
114
|
+
if ((res.status === 401 || res.status === 403) && sessionToken && hasRefreshToken && !attemptedRefresh) {
|
|
115
|
+
attemptedRefresh = true;
|
|
116
|
+
try {
|
|
117
|
+
// Use centralized internal API helper for server-to-server calls
|
|
118
|
+
const rf = await internalRefresh(req.headers.get('cookie') || '', sessionToken);
|
|
119
|
+
if (rf.ok) {
|
|
120
|
+
const fresh = await getSession(sessionToken);
|
|
121
|
+
({ res, json } = await makeCall(fresh?.idpAccessToken));
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { ok: res.ok, status: res.status, json: json as T | null, attemptedRefresh };
|
|
111
127
|
}
|