@payez/next-mvp 4.1.12 → 4.1.14
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 +0 -3
- package/dist/auth/better-auth.js +152 -36
- package/package.json +2 -2
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/auth/better-auth.ts +568 -450
|
@@ -22,9 +22,6 @@ export interface BetterAuthSocialProvider {
|
|
|
22
22
|
accessType?: 'offline' | 'online';
|
|
23
23
|
hd?: string;
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* Build Better Auth social providers from IDP config.
|
|
27
|
-
*/
|
|
28
25
|
export declare function buildBetterAuthProviders(config: IDPClientConfig): Record<string, BetterAuthSocialProvider>;
|
|
29
26
|
/**
|
|
30
27
|
* Optional configuration for `createBetterAuthInstance`.
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -53,6 +53,8 @@ exports.getBetterAuthHandler = getBetterAuthHandler;
|
|
|
53
53
|
exports.exchangeOAuthForIdpTokens = exchangeOAuthForIdpTokens;
|
|
54
54
|
exports.createAuthGetHandler = createAuthGetHandler;
|
|
55
55
|
require("server-only");
|
|
56
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
57
|
+
const jose_1 = require("jose"); // better-auth's own JWT dependency
|
|
56
58
|
const better_auth_1 = require("better-auth");
|
|
57
59
|
const next_js_1 = require("better-auth/next-js");
|
|
58
60
|
const next_js_2 = require("better-auth/next-js");
|
|
@@ -63,6 +65,30 @@ const redis_1 = require("../lib/redis");
|
|
|
63
65
|
/**
|
|
64
66
|
* Build Better Auth social providers from IDP config.
|
|
65
67
|
*/
|
|
68
|
+
/**
|
|
69
|
+
* REQUEST-SCOPED carrier for the provider's raw id_token. Card 138635.
|
|
70
|
+
*
|
|
71
|
+
* WHY THIS EXISTS: /api/ExternalAuth/oauth-callback mints an IDP bearer for any
|
|
72
|
+
* ASSERTED email — we send {provider, email} and nothing verifiable. The fix is
|
|
73
|
+
* for the IDP to require a provider-signed id_token and verify it itself. This
|
|
74
|
+
* is the client half: capture the token Better Auth already receives, and
|
|
75
|
+
* forward it. Additive — the IDP ignores it until its gate flips.
|
|
76
|
+
*
|
|
77
|
+
* WHY AsyncLocalStorage AND NOT REDIS (BAPert's ruling: forward, do not persist):
|
|
78
|
+
* an id_token is credential material. Writing it to the session record would put
|
|
79
|
+
* it one allow-list edit away from leaking out of /api/internal/session-data,
|
|
80
|
+
* and it would outlive the request that needed it. ALS scopes it to the single
|
|
81
|
+
* request, and it is gone when the request ends — nothing to expire, nothing to
|
|
82
|
+
* scrub, nothing to leak.
|
|
83
|
+
*
|
|
84
|
+
* SAFE BY CONSTRUCTION: getUserInfo (which fills this) and the post-callback
|
|
85
|
+
* exchange (which reads it) both run inside createAuthGetHandler's own request.
|
|
86
|
+
* VERIFIED, not assumed: better-auth.ts:452 awaits ba.GET(request) — the call
|
|
87
|
+
* that invokes getUserInfo — and the exchange follows at :465 in the SAME
|
|
88
|
+
* function. Outside that scope the store is undefined and the body simply omits
|
|
89
|
+
* id_token, which is the correct additive behaviour.
|
|
90
|
+
*/
|
|
91
|
+
const idTokenScope = new node_async_hooks_1.AsyncLocalStorage();
|
|
66
92
|
function buildBetterAuthProviders(config) {
|
|
67
93
|
const providers = {};
|
|
68
94
|
for (const oauth of config.oauthProviders || []) {
|
|
@@ -92,6 +118,65 @@ function buildBetterAuthProviders(config) {
|
|
|
92
118
|
accessType: rawAccessType === 'online' ? 'online' : rawAccessType === 'offline' ? 'offline' : undefined,
|
|
93
119
|
hd: typeof rawHostedDomain === 'string' ? rawHostedDomain : undefined,
|
|
94
120
|
};
|
|
121
|
+
/**
|
|
122
|
+
* CAPTURE THE RAW id_token — card 138635, client half.
|
|
123
|
+
*
|
|
124
|
+
* getUserInfo is the ONLY provider hook that receives the token set.
|
|
125
|
+
* mapProfileToUser cannot do this: it is handed the DECODED claims, and a
|
|
126
|
+
* signed JWT cannot be reconstructed from them. We need the raw token
|
|
127
|
+
* because the IDP must verify the signature ITSELF — a client-decoded
|
|
128
|
+
* claim is just another assertable field, which is the defect restated.
|
|
129
|
+
*
|
|
130
|
+
* ⚠ THIS REPLACES THE DEFAULT google getUserInfo ENTIRELY. Upstream calls
|
|
131
|
+
* `if (options.getUserInfo) return options.getUserInfo(token)` FIRST and
|
|
132
|
+
* returns our result verbatim, so everything the default did is now ours to
|
|
133
|
+
* do — including the `hd` hosted-domain restriction.
|
|
134
|
+
*
|
|
135
|
+
* `hd` IS NOT HYPOTHETICAL: it is wired from additionalParams.hd four lines
|
|
136
|
+
* above, so a client can restrict sign-in to a Google Workspace domain.
|
|
137
|
+
* Silently dropping it would let ANY Google account into a client that had
|
|
138
|
+
* restricted itself — a security regression introduced by a security fix.
|
|
139
|
+
* (Measured on 93: zero clients set it today. That is a reason to be
|
|
140
|
+
* careful, not a reason to skip it — it is configurable and unused, which
|
|
141
|
+
* is exactly the state in which a regression goes unnoticed.)
|
|
142
|
+
*
|
|
143
|
+
* SO WE DO NOT OVERRIDE AT ALL WHEN `hd` IS SET. We take the hook over
|
|
144
|
+
* ONLY where upstream's check is PROVABLY VACUOUS — its own first line is
|
|
145
|
+
* `if (!configuredHostedDomain) return true`. With no hd configured the
|
|
146
|
+
* default would return true unconditionally, so omitting it is equivalent
|
|
147
|
+
* by the library's own definition rather than by our judgement.
|
|
148
|
+
*
|
|
149
|
+
* A client that DOES set hd keeps the stock provider, keeps its domain
|
|
150
|
+
* restriction, and simply does not contribute an id_token yet. Losing a
|
|
151
|
+
* capability is an acceptable cost; silently losing a security control is
|
|
152
|
+
* not. (better-auth does not re-export the predicate through its public
|
|
153
|
+
* types, and reimplementing a security check is how two copies drift — so
|
|
154
|
+
* the honest move is to not need it.)
|
|
155
|
+
*/
|
|
156
|
+
if (name === 'google' && !providers[name].hd) {
|
|
157
|
+
providers[name].getUserInfo = async (token) => {
|
|
158
|
+
if (!token?.idToken)
|
|
159
|
+
return null;
|
|
160
|
+
// The only new behaviour: hand the raw token to this request's scope.
|
|
161
|
+
const scope = idTokenScope.getStore();
|
|
162
|
+
if (scope)
|
|
163
|
+
scope.idToken = token.idToken;
|
|
164
|
+
// Mirrors the upstream default (google.mjs) verbatim, minus the hd
|
|
165
|
+
// branch that cannot fire here — so overriding the hook changes WHAT WE
|
|
166
|
+
// KEEP, never what we ACCEPT.
|
|
167
|
+
const claims = (0, jose_1.decodeJwt)(token.idToken);
|
|
168
|
+
return {
|
|
169
|
+
user: {
|
|
170
|
+
id: claims.sub,
|
|
171
|
+
name: claims.name,
|
|
172
|
+
email: claims.email,
|
|
173
|
+
image: claims.picture,
|
|
174
|
+
emailVerified: claims.email_verified,
|
|
175
|
+
},
|
|
176
|
+
data: claims,
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
}
|
|
95
180
|
}
|
|
96
181
|
return providers;
|
|
97
182
|
}
|
|
@@ -297,6 +382,30 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
297
382
|
name,
|
|
298
383
|
image,
|
|
299
384
|
client_id: config.clientSlug || String(config.clientId),
|
|
385
|
+
/**
|
|
386
|
+
* Card 138635 — the RAW provider id_token, forwarded verbatim.
|
|
387
|
+
*
|
|
388
|
+
* Every other field in this body is an ASSERTION. The endpoint mints an
|
|
389
|
+
* IDP bearer from them today with no cryptographic check of any kind,
|
|
390
|
+
* which is why it is a complete authentication bypass and why it has
|
|
391
|
+
* been publicly reachable for 488 days.
|
|
392
|
+
*
|
|
393
|
+
* This field is the only one the IDP can VERIFY: signature against
|
|
394
|
+
* Google's JWKS, plus iss / exp / aud, and then read email_verified
|
|
395
|
+
* from the verified claims. Additive — the IDP ignores it until its
|
|
396
|
+
* gate flips, so this cannot break a login on its own.
|
|
397
|
+
*
|
|
398
|
+
* RAW, AND NOTHING DERIVED FROM IT (BAPert's ruling). We do NOT send a
|
|
399
|
+
* decoded email_verified. A client-computed verification flag is a body
|
|
400
|
+
* field with a better story: an attacker posts email_verified:true and
|
|
401
|
+
* the defect is rebuilt one layer up. The IDP verifies for itself or
|
|
402
|
+
* the fix is theatre.
|
|
403
|
+
*
|
|
404
|
+
* Absent when this runs outside the OAuth callback scope — the key is
|
|
405
|
+
* simply omitted rather than sent null, so "we never captured one" stays
|
|
406
|
+
* distinguishable from "the provider issued none".
|
|
407
|
+
*/
|
|
408
|
+
...(idTokenScope.getStore()?.idToken ? { id_token: idTokenScope.getStore().idToken } : {}),
|
|
300
409
|
}),
|
|
301
410
|
});
|
|
302
411
|
const oauthResText = await oauthRes.text();
|
|
@@ -380,45 +489,52 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
380
489
|
*/
|
|
381
490
|
function createAuthGetHandler(loginPath = '/account-auth/login') {
|
|
382
491
|
return async function GET(request) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
|
|
393
|
-
return Response.redirect(new URL(loginPath, request.url), 302);
|
|
492
|
+
// Card 138635: establish the request-scoped carrier for the provider's raw
|
|
493
|
+
// id_token. It must wrap BOTH ba.GET (which invokes getUserInfo, the only
|
|
494
|
+
// hook that sees the token) and the exchange below (which forwards it).
|
|
495
|
+
// Wrapping the whole handler is what makes "never persisted" achievable —
|
|
496
|
+
// the token exists only for the life of this request.
|
|
497
|
+
return idTokenScope.run({}, async () => {
|
|
498
|
+
const ba = await getBetterAuthHandler();
|
|
499
|
+
if (!ba) {
|
|
500
|
+
return new Response('Auth handler not configured', { status: 500 });
|
|
394
501
|
}
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
const setCookies = response.headers.getSetCookie?.() || [];
|
|
403
|
-
const cookieHeader = setCookies
|
|
404
|
-
.map((c) => c.split(';')[0])
|
|
405
|
-
.join('; ');
|
|
406
|
-
const headers = new Headers();
|
|
407
|
-
headers.set('cookie', cookieHeader);
|
|
408
|
-
const session = await auth.api.getSession({ headers });
|
|
409
|
-
if (session?.session?.token) {
|
|
410
|
-
console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
|
|
411
|
-
await exchangeOAuthForIdpTokens(session.session.token);
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
414
|
-
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
415
|
-
}
|
|
502
|
+
const response = await ba.GET(request);
|
|
503
|
+
// Intercept auth errors (state mismatch, expired cookies) — redirect to login cleanly
|
|
504
|
+
if (response.status === 302) {
|
|
505
|
+
const location = response.headers.get('location') || '';
|
|
506
|
+
if (location.includes('/api/auth/error') || location.includes('please_restart')) {
|
|
507
|
+
console.warn('[BETTER_AUTH] OAuth state error, redirecting to login');
|
|
508
|
+
return Response.redirect(new URL(loginPath, request.url), 302);
|
|
416
509
|
}
|
|
417
510
|
}
|
|
418
|
-
|
|
419
|
-
|
|
511
|
+
// After successful OAuth callback: exchange Google identity for IDP tokens
|
|
512
|
+
const url = new URL(request.url);
|
|
513
|
+
if (url.pathname.includes('/callback/') && response.status === 302) {
|
|
514
|
+
try {
|
|
515
|
+
const auth = await getBetterAuthInstance();
|
|
516
|
+
if (auth?.api?.getSession) {
|
|
517
|
+
const setCookies = response.headers.getSetCookie?.() || [];
|
|
518
|
+
const cookieHeader = setCookies
|
|
519
|
+
.map((c) => c.split(';')[0])
|
|
520
|
+
.join('; ');
|
|
521
|
+
const headers = new Headers();
|
|
522
|
+
headers.set('cookie', cookieHeader);
|
|
523
|
+
const session = await auth.api.getSession({ headers });
|
|
524
|
+
if (session?.session?.token) {
|
|
525
|
+
console.log('[BETTER_AUTH] Got session token from callback:', session.session.token.substring(0, 10), '| email:', session.user?.email);
|
|
526
|
+
await exchangeOAuthForIdpTokens(session.session.token);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
console.warn('[BETTER_AUTH] Could not get session after OAuth callback');
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', err.message);
|
|
535
|
+
}
|
|
420
536
|
}
|
|
421
|
-
|
|
422
|
-
|
|
537
|
+
return response;
|
|
538
|
+
});
|
|
423
539
|
};
|
|
424
540
|
}
|
package/package.json
CHANGED
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
# CI Check: Detect incorrect internal API URL patterns
|
|
4
|
-
#
|
|
5
|
-
# This script fails if code is using req.url or req.nextUrl.origin to construct
|
|
6
|
-
# URLs for internal API calls. These patterns cause 403 errors in Kubernetes
|
|
7
|
-
# because they use the external HTTPS URL instead of INTERNAL_API_URL.
|
|
8
|
-
#
|
|
9
|
-
# CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
|
|
10
|
-
# INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
|
|
11
|
-
#
|
|
12
|
-
# NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
|
|
13
|
-
# The problem is when you construct a NEW path using req.url as the base.
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
set -e
|
|
17
|
-
|
|
18
|
-
SEARCH_PATH="${1:-.}"
|
|
19
|
-
ERRORS_FOUND=0
|
|
20
|
-
|
|
21
|
-
echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
|
|
22
|
-
echo "========================================================================"
|
|
23
|
-
|
|
24
|
-
# Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
|
|
25
|
-
# This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
|
|
26
|
-
# But NOT: new URL(req.url) which is just parsing the request
|
|
27
|
-
echo ""
|
|
28
|
-
echo "Checking for: new URL('/api/...', req.url) pattern"
|
|
29
|
-
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
30
|
-
echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
|
|
31
|
-
ERRORS_FOUND=1
|
|
32
|
-
fi
|
|
33
|
-
|
|
34
|
-
# Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
|
|
35
|
-
echo ""
|
|
36
|
-
echo "Checking for: new URL(..., req.nextUrl) pattern"
|
|
37
|
-
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
38
|
-
echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
|
|
39
|
-
ERRORS_FOUND=1
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
# Pattern 3: fetch with req.nextUrl.origin and /api/
|
|
43
|
-
echo ""
|
|
44
|
-
echo "Checking for: fetch with req.nextUrl.origin and /api/"
|
|
45
|
-
if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
46
|
-
echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
|
|
47
|
-
ERRORS_FOUND=1
|
|
48
|
-
fi
|
|
49
|
-
|
|
50
|
-
# Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
|
|
51
|
-
echo ""
|
|
52
|
-
echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
|
|
53
|
-
if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
|
|
54
|
-
echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
|
|
55
|
-
# Not failing on this one as it might be intentional in some cases (client-side refresh)
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
echo ""
|
|
59
|
-
echo "========================================================================"
|
|
60
|
-
|
|
61
|
-
if [ $ERRORS_FOUND -eq 1 ]; then
|
|
62
|
-
echo "FAILED: Dangerous internal URL patterns detected!"
|
|
63
|
-
echo ""
|
|
64
|
-
echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
|
|
65
|
-
echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
|
|
66
|
-
echo ""
|
|
67
|
-
echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
|
|
68
|
-
echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
|
|
69
|
-
exit 1
|
|
70
|
-
else
|
|
71
|
-
echo "PASSED: No dangerous internal URL patterns found."
|
|
72
|
-
exit 0
|
|
73
|
-
fi
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
#
|
|
3
|
+
# CI Check: Detect incorrect internal API URL patterns
|
|
4
|
+
#
|
|
5
|
+
# This script fails if code is using req.url or req.nextUrl.origin to construct
|
|
6
|
+
# URLs for internal API calls. These patterns cause 403 errors in Kubernetes
|
|
7
|
+
# because they use the external HTTPS URL instead of INTERNAL_API_URL.
|
|
8
|
+
#
|
|
9
|
+
# CORRECT: Use internalFetch() or internalRefresh() from @payez/next-mvp/lib/internal-api
|
|
10
|
+
# INCORRECT: new URL('/api/...', req.url) or fetch(`${req.nextUrl.origin}/api/...`)
|
|
11
|
+
#
|
|
12
|
+
# NOTE: new URL(req.url) alone is FINE - it just parses the incoming request URL.
|
|
13
|
+
# The problem is when you construct a NEW path using req.url as the base.
|
|
14
|
+
#
|
|
15
|
+
|
|
16
|
+
set -e
|
|
17
|
+
|
|
18
|
+
SEARCH_PATH="${1:-.}"
|
|
19
|
+
ERRORS_FOUND=0
|
|
20
|
+
|
|
21
|
+
echo "Checking for incorrect internal URL patterns in: $SEARCH_PATH"
|
|
22
|
+
echo "========================================================================"
|
|
23
|
+
|
|
24
|
+
# Pattern 1: new URL('/api/...', req.url) - constructing internal API URL from request
|
|
25
|
+
# This catches: new URL('/api/...', req.url) and new URL("/api/...", req.url)
|
|
26
|
+
# But NOT: new URL(req.url) which is just parsing the request
|
|
27
|
+
echo ""
|
|
28
|
+
echo "Checking for: new URL('/api/...', req.url) pattern"
|
|
29
|
+
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\(['\"][^'\"]*['\"],\s*req\.url" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
30
|
+
echo "ERROR: Found 'new URL(path, req.url)' pattern. Use internalFetch() instead."
|
|
31
|
+
ERRORS_FOUND=1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Pattern 2: new URL('/api/...', req.nextUrl) or new URL(..., req.nextUrl.origin)
|
|
35
|
+
echo ""
|
|
36
|
+
echo "Checking for: new URL(..., req.nextUrl) pattern"
|
|
37
|
+
if grep -rn --include="*.ts" --include="*.tsx" -E "new URL\([^)]+,\s*req\.nextUrl" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
38
|
+
echo "ERROR: Found 'new URL(..., req.nextUrl)' pattern. Use internalFetch() instead."
|
|
39
|
+
ERRORS_FOUND=1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Pattern 3: fetch with req.nextUrl.origin and /api/
|
|
43
|
+
echo ""
|
|
44
|
+
echo "Checking for: fetch with req.nextUrl.origin and /api/"
|
|
45
|
+
if grep -rn --include="*.ts" --include="*.tsx" "req\.nextUrl\.origin.*\/api\/" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts"; then
|
|
46
|
+
echo "ERROR: Found 'req.nextUrl.origin' used with internal API route. Use internalFetch() instead."
|
|
47
|
+
ERRORS_FOUND=1
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Pattern 4: Direct fetch to /api/auth/refresh without using internalRefresh (warning only)
|
|
51
|
+
echo ""
|
|
52
|
+
echo "Checking for: Direct fetch to /api/auth/refresh (should use internalRefresh)"
|
|
53
|
+
if grep -rn --include="*.ts" --include="*.tsx" "fetch.*\/api\/auth\/refresh" "$SEARCH_PATH" 2>/dev/null | grep -v "node_modules" | grep -v "\.d\.ts" | grep -v "internal-api\.ts"; then
|
|
54
|
+
echo "WARNING: Found direct fetch to /api/auth/refresh. Consider using internalRefresh() helper."
|
|
55
|
+
# Not failing on this one as it might be intentional in some cases (client-side refresh)
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
echo ""
|
|
59
|
+
echo "========================================================================"
|
|
60
|
+
|
|
61
|
+
if [ $ERRORS_FOUND -eq 1 ]; then
|
|
62
|
+
echo "FAILED: Dangerous internal URL patterns detected!"
|
|
63
|
+
echo ""
|
|
64
|
+
echo "Fix: Import and use helpers from @payez/next-mvp/lib/internal-api:"
|
|
65
|
+
echo " import { internalFetch, internalRefresh } from '@payez/next-mvp/lib/internal-api';"
|
|
66
|
+
echo ""
|
|
67
|
+
echo "Why: Server-to-server calls must use INTERNAL_API_URL (http://service.namespace.svc.cluster.local)"
|
|
68
|
+
echo " not the external URL from the request (https://api.example.com) which fails in Kubernetes."
|
|
69
|
+
exit 1
|
|
70
|
+
else
|
|
71
|
+
echo "PASSED: No dangerous internal URL patterns found."
|
|
72
|
+
exit 0
|
|
73
|
+
fi
|