@payez/next-mvp 4.1.10 → 4.1.12
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/api-handlers/auth/refresh.js +15 -38
- package/dist/auth/better-auth.js +9 -18
- package/dist/client/better-auth-client.d.ts +14 -2
- package/dist/lib/ensure-fresh-access-token.js +7 -30
- package/dist/lib/session-store.d.ts +0 -1
- package/dist/lib/session-store.js +29 -33
- package/dist/lib/startup-init.js +19 -19
- package/package.json +4 -3
- package/scripts/check-internal-url-usage.sh +73 -73
- package/src/api/index.ts +17 -17
- package/src/api-handlers/account/masked-info.ts +45 -45
- package/src/api-handlers/account/profile.ts +80 -80
- package/src/api-handlers/account/recovery/initiate.ts +23 -23
- package/src/api-handlers/account/recovery/send-code.ts +25 -25
- package/src/api-handlers/account/recovery/verify-code.ts +25 -25
- package/src/api-handlers/account/reset-password.ts +23 -23
- package/src/api-handlers/account/send-code.ts +76 -76
- package/src/api-handlers/account/validate-password.ts +118 -118
- package/src/api-handlers/admin/stats.ts +249 -249
- package/src/api-handlers/anon/preferences.ts +123 -123
- package/src/api-handlers/auth/jwks.ts +20 -20
- package/src/api-handlers/auth/login.ts +240 -240
- package/src/api-handlers/auth/refresh.ts +15 -38
- package/src/api-handlers/auth/validate.ts +43 -43
- package/src/api-handlers/session/viability.ts +142 -142
- package/src/auth/better-auth.ts +9 -14
- package/src/auth/unauthenticated-routes.ts +2 -2
- package/src/client/better-auth-client.ts +70 -70
- package/src/client/fetchWithSession.ts +21 -21
- package/src/client/index.ts +13 -13
- package/src/client/useAnonSession.ts +131 -131
- package/src/components/admin/VibeAdminContext.tsx +87 -87
- package/src/config/logging-config.ts +82 -82
- package/src/config/unauthenticated-routes.ts +19 -19
- package/src/edge/internal-api-url.ts +65 -65
- package/src/edge/middleware.ts +42 -42
- package/src/hooks/useProfile.ts +75 -75
- package/src/lib/anon-session.ts +213 -213
- package/src/lib/auth-secret.ts +137 -137
- package/src/lib/demo-mode.ts +17 -17
- package/src/lib/ensure-fresh-access-token.ts +7 -30
- package/src/lib/idp-client-config.ts +559 -559
- package/src/lib/idp-fetch.ts +126 -126
- package/src/lib/internal-api.ts +171 -171
- package/src/lib/jwt-decode-client.ts +45 -45
- package/src/lib/rate-limit-service.ts +9 -9
- package/src/lib/redis.ts +26 -26
- package/src/lib/refresh-token-validator.ts +64 -64
- package/src/lib/secret-validation.ts +8 -8
- package/src/lib/session-store.ts +692 -695
- package/src/lib/session.ts +34 -34
- package/src/lib/startup-init.ts +251 -251
- package/src/lib/token-expiry.ts +40 -40
- package/src/lib/token-lifecycle.ts +470 -470
- package/src/lib/types/api-responses.ts +336 -336
- package/src/middleware/auth-decision.ts +43 -43
- package/src/models/DecodedAccessToken.ts +16 -16
- package/src/models/SessionModel.ts +264 -264
- package/src/pages/admin-roles/index.ts +9 -9
- package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
- package/src/routes/account/index.ts +31 -31
- package/src/routes/account/masked-info.ts +36 -36
- package/src/routes/account/send-code.ts +39 -39
- package/src/routes/account/verify-email.ts +40 -40
- package/src/routes/account/verify-sms.ts +40 -40
- package/src/routes/auth/index.ts +22 -22
- package/src/routes/auth/refresh.ts +48 -48
- package/src/routes/auth/session.ts +166 -166
- package/src/routes/index.ts +17 -17
- package/src/routes/session/index.ts +7 -7
- package/src/routes/session/refresh-viability.ts +17 -17
- package/src/server/auth.ts +373 -373
- package/src/server/decode-session.ts +202 -202
- package/src/services/signalrActivityService.ts +258 -258
- package/src/theme/ThemeProvider.tsx +39 -39
- package/src/theme/default.ts +33 -33
- package/src/theme/index.ts +31 -31
- package/src/theme/types.ts +69 -69
- package/src/theme/useTheme.ts +57 -57
- package/src/theme/utils.ts +40 -40
- package/src/types/api.ts +12 -12
- package/src/types/auth.d.ts +23 -23
- package/src/types/auth.ts +22 -22
- package/src/types/logging.ts +11 -11
- package/src/types/recovery.ts +54 -54
- package/src/utils/api.ts +353 -353
- package/src/utils/circuitBreaker.ts +40 -40
- package/src/utils/error-message.ts +108 -108
- package/src/utils/layout/reservedSpace.ts +124 -124
|
@@ -371,7 +371,7 @@ function createRefreshHandler(config) {
|
|
|
371
371
|
const errorMessage = idpError.message || 'Token refresh failed';
|
|
372
372
|
// CRITICAL: 401 means token is definitively invalid - always discard
|
|
373
373
|
// This prevents redirect loops where viability check keeps returning canRefresh:true
|
|
374
|
-
|
|
374
|
+
const discardToken = refreshResponse.status === 401 || idpError.discard_token === true;
|
|
375
375
|
const retryable = refreshResponse.status !== 401 && idpError.retryable === true;
|
|
376
376
|
const resolution = idpError.resolution || null;
|
|
377
377
|
// ============================================================================
|
|
@@ -408,43 +408,20 @@ function createRefreshHandler(config) {
|
|
|
408
408
|
console.error('[AUTH_REFRESH] Full IDP error response:', JSON.stringify(responseData, null, 2).substring(0, 1000));
|
|
409
409
|
// If IDP signals to discard token, clear it from Redis
|
|
410
410
|
if (discardToken) {
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
});
|
|
426
|
-
await (0, session_store_1.updateSession)(sessionToken, {
|
|
427
|
-
idpRefreshToken: '',
|
|
428
|
-
idpRefreshTokenExpires: undefined,
|
|
429
|
-
refreshTokenClearedAt: Date.now(),
|
|
430
|
-
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
else {
|
|
434
|
-
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
435
|
-
// freshly-rotated token directly so the loser succeeds without a retry
|
|
436
|
-
// (race-fix follow-up #1).
|
|
437
|
-
discardToken = false;
|
|
438
|
-
console.warn('[AUTH_REFRESH] Discard suppressed: refresh token already rotated by a concurrent refresh — returning the fresh token (race-safe).');
|
|
439
|
-
if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
|
|
440
|
-
return server_1.NextResponse.json({
|
|
441
|
-
refreshed: true,
|
|
442
|
-
reason: 'completed_by_concurrent_request',
|
|
443
|
-
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
444
|
-
hasRefreshToken: !!postRefresh.idpRefreshToken,
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
}
|
|
411
|
+
console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: IDP signaled discard_token', {
|
|
412
|
+
reason: 'IDP_DISCARD_TOKEN',
|
|
413
|
+
errorCode,
|
|
414
|
+
userId,
|
|
415
|
+
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
416
|
+
hadRefreshToken: !!currentSession.idpRefreshToken,
|
|
417
|
+
stack: new Error().stack
|
|
418
|
+
});
|
|
419
|
+
await (0, session_store_1.updateSession)(sessionToken, {
|
|
420
|
+
idpRefreshToken: '',
|
|
421
|
+
idpRefreshTokenExpires: undefined,
|
|
422
|
+
refreshTokenClearedAt: Date.now(),
|
|
423
|
+
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
|
|
424
|
+
});
|
|
448
425
|
}
|
|
449
426
|
return server_1.NextResponse.json({
|
|
450
427
|
error: errorMessage,
|
package/dist/auth/better-auth.js
CHANGED
|
@@ -60,7 +60,6 @@ const magic_link_1 = require("better-auth/plugins/magic-link");
|
|
|
60
60
|
const idp_client_config_1 = require("../lib/idp-client-config");
|
|
61
61
|
const app_slug_1 = require("../lib/app-slug");
|
|
62
62
|
const redis_1 = require("../lib/redis");
|
|
63
|
-
const session_store_1 = require("../lib/session-store");
|
|
64
63
|
/**
|
|
65
64
|
* Build Better Auth social providers from IDP config.
|
|
66
65
|
*/
|
|
@@ -126,24 +125,16 @@ function createBetterAuthInstance(idpConfig, opts = {}) {
|
|
|
126
125
|
// Redis-backed session storage via secondaryStorage
|
|
127
126
|
secondaryStorage: {
|
|
128
127
|
get: async (key) => {
|
|
129
|
-
|
|
130
|
-
return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
128
|
+
return await (0, redis_1.getRedis)().get(`ba:${appSlug}:${key}`);
|
|
135
129
|
},
|
|
136
130
|
set: async (key, value, ttl) => {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
await redis.setex(`ba:${appSlug}:${key}`, session_store_1.SESSION_TTL, value);
|
|
144
|
-
}
|
|
131
|
+
const redis = (0, redis_1.getRedis)();
|
|
132
|
+
if (ttl) {
|
|
133
|
+
await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
await redis.setex(`ba:${appSlug}:${key}`, 7 * 24 * 60 * 60, value);
|
|
145
137
|
}
|
|
146
|
-
catch { /* Redis unavailable — cookie cache still works */ }
|
|
147
138
|
},
|
|
148
139
|
delete: async (key) => {
|
|
149
140
|
try {
|
|
@@ -348,13 +339,13 @@ async function exchangeOAuthForIdpTokens(sessionToken, provider = 'google') {
|
|
|
348
339
|
};
|
|
349
340
|
// Store in BA Redis session (for decodeSession)
|
|
350
341
|
baData.idpTokens = idpTokenData;
|
|
351
|
-
await (0, redis_1.getRedis)().setex(baKey,
|
|
342
|
+
await (0, redis_1.getRedis)().setex(baKey, 7 * 24 * 60 * 60, JSON.stringify(baData));
|
|
352
343
|
// Write to canonical session store so refresh handler and token lifecycle can find the tokens.
|
|
353
344
|
// Key format: {sessionPrefix}{token} — same key that getSession() reads from.
|
|
354
345
|
try {
|
|
355
346
|
const { getSessionPrefix } = await Promise.resolve().then(() => __importStar(require('../lib/app-slug')));
|
|
356
347
|
const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
|
|
357
|
-
await (0, redis_1.getRedis)().setex(canonicalKey,
|
|
348
|
+
await (0, redis_1.getRedis)().setex(canonicalKey, 7 * 24 * 60 * 60, JSON.stringify({
|
|
358
349
|
...idpTokenData,
|
|
359
350
|
oauthProvider: provider,
|
|
360
351
|
}));
|
|
@@ -814,7 +814,13 @@ export declare const authClient: {
|
|
|
814
814
|
isPending: boolean;
|
|
815
815
|
isRefetching: boolean;
|
|
816
816
|
error: import("@better-fetch/fetch").BetterFetchError | null;
|
|
817
|
-
refetch: (queryParams
|
|
817
|
+
refetch: (queryParams
|
|
818
|
+
/**
|
|
819
|
+
* NextAuth-compatible signOut wrapper.
|
|
820
|
+
*
|
|
821
|
+
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
822
|
+
*/
|
|
823
|
+
?: {
|
|
818
824
|
query?: import("better-auth/types").SessionQueryParams;
|
|
819
825
|
} | undefined) => Promise<void>;
|
|
820
826
|
};
|
|
@@ -988,7 +994,13 @@ export declare const useSession: () => {
|
|
|
988
994
|
isPending: boolean;
|
|
989
995
|
isRefetching: boolean;
|
|
990
996
|
error: import("@better-fetch/fetch").BetterFetchError | null;
|
|
991
|
-
refetch: (queryParams
|
|
997
|
+
refetch: (queryParams
|
|
998
|
+
/**
|
|
999
|
+
* NextAuth-compatible signOut wrapper.
|
|
1000
|
+
*
|
|
1001
|
+
* Maps NextAuth signOut({ redirect, callbackUrl }) to Better Auth.
|
|
1002
|
+
*/
|
|
1003
|
+
?: {
|
|
992
1004
|
query?: import("better-auth/types").SessionQueryParams;
|
|
993
1005
|
} | undefined) => Promise<void>;
|
|
994
1006
|
}, signIn: {
|
|
@@ -200,38 +200,15 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
|
|
|
200
200
|
if (!idpResponse.ok) {
|
|
201
201
|
const idpError = responseData?.error || {};
|
|
202
202
|
const code = idpError.code || 'UNKNOWN_ERROR';
|
|
203
|
-
|
|
203
|
+
const discardToken = idpResponse.status === 401 || idpError.discard_token === true;
|
|
204
204
|
const retryable = idpResponse.status !== 401 && idpError.retryable === true;
|
|
205
205
|
if (discardToken) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const tokenUnchanged = !!session.idpRefreshToken && postRefresh?.idpRefreshToken === session.idpRefreshToken;
|
|
213
|
-
if (tokenUnchanged) {
|
|
214
|
-
await (0, session_store_1.updateSession)(sessionToken, {
|
|
215
|
-
idpRefreshToken: '',
|
|
216
|
-
idpRefreshTokenExpires: undefined,
|
|
217
|
-
refreshTokenClearedAt: Date.now(),
|
|
218
|
-
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
223
|
-
// freshly-rotated token directly so the loser succeeds without a retry round-trip
|
|
224
|
-
// (race-fix follow-up #1).
|
|
225
|
-
discardToken = false;
|
|
226
|
-
if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
|
|
227
|
-
return {
|
|
228
|
-
ok: true,
|
|
229
|
-
accessToken: postRefresh.idpAccessToken,
|
|
230
|
-
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
231
|
-
refreshed: true,
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
}
|
|
206
|
+
await (0, session_store_1.updateSession)(sessionToken, {
|
|
207
|
+
idpRefreshToken: '',
|
|
208
|
+
idpRefreshTokenExpires: undefined,
|
|
209
|
+
refreshTokenClearedAt: Date.now(),
|
|
210
|
+
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${code}`,
|
|
211
|
+
});
|
|
235
212
|
}
|
|
236
213
|
return {
|
|
237
214
|
ok: false,
|
|
@@ -12,7 +12,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
13
|
};
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.SESSION_TTL = void 0;
|
|
16
15
|
exports.generateSessionToken = generateSessionToken;
|
|
17
16
|
exports.createSession = createSession;
|
|
18
17
|
exports.getSession = getSession;
|
|
@@ -47,10 +46,7 @@ const getSessionVersionKey = (token) => `${(0, app_slug_1.getSessionPrefix)()}ve
|
|
|
47
46
|
// Better Auth uses a different key format: ba:{appSlug}:{token}
|
|
48
47
|
const getBetterAuthSessionKey = (token, appSlug) => `ba:${appSlug || 'app'}:${token}`;
|
|
49
48
|
const REFRESH_LOCK_TTL = 60; // 60 seconds
|
|
50
|
-
// 3 days in seconds (matches refresh token lifetime)
|
|
51
|
-
// of session TTL — better-auth.ts derives its BA-session Redis TTL from this so the
|
|
52
|
-
// two can never drift apart again (the 7d-vs-3d drift = the zombie-session window, QA 6952).
|
|
53
|
-
exports.SESSION_TTL = 3 * 24 * 60 * 60;
|
|
49
|
+
const SESSION_TTL = 3 * 24 * 60 * 60; // 3 days in seconds (matches refresh token lifetime)
|
|
54
50
|
/**
|
|
55
51
|
* Generates a new session token.
|
|
56
52
|
* @returns A new session token string.
|
|
@@ -70,8 +66,8 @@ async function createSession(data) {
|
|
|
70
66
|
const versionKey = getSessionVersionKey(sessionToken);
|
|
71
67
|
try {
|
|
72
68
|
await redis_1.default.multi()
|
|
73
|
-
.setex(key,
|
|
74
|
-
.setex(versionKey,
|
|
69
|
+
.setex(key, SESSION_TTL, JSON.stringify(data))
|
|
70
|
+
.setex(versionKey, SESSION_TTL, '1')
|
|
75
71
|
.exec();
|
|
76
72
|
}
|
|
77
73
|
catch (error) {
|
|
@@ -152,7 +148,7 @@ async function getBetterAuthSession(sessionToken, appSlug) {
|
|
|
152
148
|
*/
|
|
153
149
|
async function touchSession(token) {
|
|
154
150
|
const key = getSessionKey(token);
|
|
155
|
-
await redis_1.default.expire(key,
|
|
151
|
+
await redis_1.default.expire(key, SESSION_TTL);
|
|
156
152
|
}
|
|
157
153
|
/**
|
|
158
154
|
* Retrieves a session along with a version identifier for optimistic locking.
|
|
@@ -218,9 +214,9 @@ async function setSession(sessionToken, data) {
|
|
|
218
214
|
const key = getSessionKey(sessionToken);
|
|
219
215
|
const versionKey = getSessionVersionKey(sessionToken);
|
|
220
216
|
await redis_1.default.multi()
|
|
221
|
-
.setex(key,
|
|
217
|
+
.setex(key, SESSION_TTL, JSON.stringify(data))
|
|
222
218
|
.incr(versionKey)
|
|
223
|
-
.expire(versionKey,
|
|
219
|
+
.expire(versionKey, SESSION_TTL)
|
|
224
220
|
.exec();
|
|
225
221
|
}
|
|
226
222
|
/**
|
|
@@ -258,9 +254,9 @@ async function updateSession(sessionToken, updates) {
|
|
|
258
254
|
const updatedSession = { ...currentSession, ...updates };
|
|
259
255
|
// Write the entire updated session back to Redis with version increment
|
|
260
256
|
await redis_1.default.multi()
|
|
261
|
-
.setex(key,
|
|
257
|
+
.setex(key, SESSION_TTL, JSON.stringify(updatedSession))
|
|
262
258
|
.incr(versionKey)
|
|
263
|
-
.expire(versionKey,
|
|
259
|
+
.expire(versionKey, SESSION_TTL)
|
|
264
260
|
.exec();
|
|
265
261
|
return updatedSession;
|
|
266
262
|
}
|
|
@@ -489,27 +485,27 @@ async function releaseRefreshLock(sessionToken, requestId, lockVersion) {
|
|
|
489
485
|
const lockKey = getRefreshLockKey(sessionToken);
|
|
490
486
|
try {
|
|
491
487
|
// Lua script for atomic lock validation and release
|
|
492
|
-
const luaScript = `
|
|
493
|
-
local lockKey = KEYS[1]
|
|
494
|
-
local expectedRequestId = ARGV[1]
|
|
495
|
-
local expectedVersion = ARGV[2]
|
|
496
|
-
|
|
497
|
-
local lockData = redis.call('GET', lockKey)
|
|
498
|
-
if not lockData then
|
|
499
|
-
return 0 -- Lock doesn't exist
|
|
500
|
-
end
|
|
501
|
-
|
|
502
|
-
local lockInfo = cjson.decode(lockData)
|
|
503
|
-
if lockInfo.acquiredBy == expectedRequestId then
|
|
504
|
-
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
505
|
-
redis.call('DEL', lockKey)
|
|
506
|
-
return 1 -- Successfully released
|
|
507
|
-
else
|
|
508
|
-
return -2 -- Version mismatch
|
|
509
|
-
end
|
|
510
|
-
else
|
|
511
|
-
return -1 -- Wrong owner
|
|
512
|
-
end
|
|
488
|
+
const luaScript = `
|
|
489
|
+
local lockKey = KEYS[1]
|
|
490
|
+
local expectedRequestId = ARGV[1]
|
|
491
|
+
local expectedVersion = ARGV[2]
|
|
492
|
+
|
|
493
|
+
local lockData = redis.call('GET', lockKey)
|
|
494
|
+
if not lockData then
|
|
495
|
+
return 0 -- Lock doesn't exist
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
local lockInfo = cjson.decode(lockData)
|
|
499
|
+
if lockInfo.acquiredBy == expectedRequestId then
|
|
500
|
+
if not expectedVersion or expectedVersion == '' or tostring(lockInfo.lockVersion) == expectedVersion then
|
|
501
|
+
redis.call('DEL', lockKey)
|
|
502
|
+
return 1 -- Successfully released
|
|
503
|
+
else
|
|
504
|
+
return -2 -- Version mismatch
|
|
505
|
+
end
|
|
506
|
+
else
|
|
507
|
+
return -1 -- Wrong owner
|
|
508
|
+
end
|
|
513
509
|
`;
|
|
514
510
|
const result = await redis_1.default.eval(luaScript, 1, lockKey, requestId, lockVersion ? lockVersion.toString() : '');
|
|
515
511
|
if (result === 1) {
|
package/dist/lib/startup-init.js
CHANGED
|
@@ -213,25 +213,25 @@ async function performInitialization() {
|
|
|
213
213
|
const connectionLine = isConnectionError
|
|
214
214
|
? '║ 🔌 CONNECTION REFUSED - IDP appears to be down ║\n║ ║\n'
|
|
215
215
|
: '';
|
|
216
|
-
console.error(`
|
|
217
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
218
|
-
║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
|
|
219
|
-
║ ║
|
|
220
|
-
║ The app cannot start without a valid auth signing secret. ║
|
|
221
|
-
║ This should be fetched from IDP at startup. ║
|
|
222
|
-
║ ║
|
|
223
|
-
${connectionLine}║ Possible causes: ║
|
|
224
|
-
║ • IDP is not running or unreachable ║
|
|
225
|
-
║ • CLIENT_ID is not registered in IDP ║
|
|
226
|
-
║ • IDP_URL is incorrect ║
|
|
227
|
-
║ • Network connectivity issue ║
|
|
228
|
-
║ ║
|
|
229
|
-
║ Current config: ║
|
|
230
|
-
║ • IDP_URL: ${idpUrl}║
|
|
231
|
-
║ • CLIENT_ID: ${clientId}║
|
|
232
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
233
|
-
|
|
234
|
-
[STARTUP] Error: ${errorMsg}
|
|
216
|
+
console.error(`
|
|
217
|
+
╔══════════════════════════════════════════════════════════════╗
|
|
218
|
+
║ ❌ FATAL: BETTER_AUTH_SECRET NOT AVAILABLE ║
|
|
219
|
+
║ ║
|
|
220
|
+
║ The app cannot start without a valid auth signing secret. ║
|
|
221
|
+
║ This should be fetched from IDP at startup. ║
|
|
222
|
+
║ ║
|
|
223
|
+
${connectionLine}║ Possible causes: ║
|
|
224
|
+
║ • IDP is not running or unreachable ║
|
|
225
|
+
║ • CLIENT_ID is not registered in IDP ║
|
|
226
|
+
║ • IDP_URL is incorrect ║
|
|
227
|
+
║ • Network connectivity issue ║
|
|
228
|
+
║ ║
|
|
229
|
+
║ Current config: ║
|
|
230
|
+
║ • IDP_URL: ${idpUrl}║
|
|
231
|
+
║ • CLIENT_ID: ${clientId}║
|
|
232
|
+
╚══════════════════════════════════════════════════════════════╝
|
|
233
|
+
|
|
234
|
+
[STARTUP] Error: ${errorMsg}
|
|
235
235
|
`);
|
|
236
236
|
// Re-throw so callers know initialization failed
|
|
237
237
|
throw lastInitError;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@payez/next-mvp",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.12",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
"src"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "npx tsc --project tsconfig.json",
|
|
13
|
+
"build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true})\" && npx tsc --project tsconfig.json",
|
|
14
14
|
"prepublishOnly": "npm run build",
|
|
15
15
|
"dev": "next dev",
|
|
16
16
|
"dev:local": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-local.ps1",
|
|
17
|
-
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1"
|
|
17
|
+
"dev:broker": "pwsh -NoProfile -ExecutionPolicy Bypass -File scripts/dev-broker.ps1",
|
|
18
|
+
"prepack": "npm run build"
|
|
18
19
|
},
|
|
19
20
|
"exports": {
|
|
20
21
|
".": {
|
|
@@ -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
|
package/src/api/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @payez/next-mvp API Module Exports
|
|
3
|
-
*
|
|
4
|
-
* Provides enhanced API route handlers with automatic token management
|
|
5
|
-
*
|
|
6
|
-
* @version 2.0.0
|
|
7
|
-
* @since auth-ready-v2
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export {
|
|
11
|
-
createAuthHandler,
|
|
12
|
-
type AuthContext,
|
|
13
|
-
type AuthHandlerOptions,
|
|
14
|
-
type HandlerFunction
|
|
15
|
-
} from './auth-handler';
|
|
16
|
-
|
|
17
|
-
// Default export for convenience
|
|
1
|
+
/**
|
|
2
|
+
* @payez/next-mvp API Module Exports
|
|
3
|
+
*
|
|
4
|
+
* Provides enhanced API route handlers with automatic token management
|
|
5
|
+
*
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
* @since auth-ready-v2
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export {
|
|
11
|
+
createAuthHandler,
|
|
12
|
+
type AuthContext,
|
|
13
|
+
type AuthHandlerOptions,
|
|
14
|
+
type HandlerFunction
|
|
15
|
+
} from './auth-handler';
|
|
16
|
+
|
|
17
|
+
// Default export for convenience
|
|
18
18
|
export { default } from './auth-handler';
|