@payez/next-mvp 4.1.8 → 4.1.10
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 +38 -15
- package/dist/auth/better-auth.js +4 -3
- package/dist/lib/ensure-fresh-access-token.js +30 -7
- package/dist/lib/session-store.d.ts +1 -0
- package/dist/lib/session-store.js +12 -8
- package/package.json +893 -893
- package/src/api-handlers/auth/refresh.ts +38 -15
- package/src/auth/better-auth.ts +4 -3
- package/src/lib/ensure-fresh-access-token.ts +358 -335
- package/src/lib/session-store.ts +4 -1
|
@@ -400,7 +400,7 @@ export function createRefreshHandler(config: RefreshConfig) {
|
|
|
400
400
|
const errorMessage = idpError.message || 'Token refresh failed';
|
|
401
401
|
// CRITICAL: 401 means token is definitively invalid - always discard
|
|
402
402
|
// This prevents redirect loops where viability check keeps returning canRefresh:true
|
|
403
|
-
|
|
403
|
+
let discardToken = refreshResponse.status === 401 || idpError.discard_token === true;
|
|
404
404
|
const retryable = refreshResponse.status !== 401 && idpError.retryable === true;
|
|
405
405
|
const resolution = idpError.resolution || null;
|
|
406
406
|
|
|
@@ -442,20 +442,43 @@ export function createRefreshHandler(config: RefreshConfig) {
|
|
|
442
442
|
|
|
443
443
|
// If IDP signals to discard token, clear it from Redis
|
|
444
444
|
if (discardToken) {
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
445
|
+
// Race-safe discard (CAS): only clear if the refresh token we sent is STILL
|
|
446
|
+
// the current one. A concurrent refresh may have already rotated it — our 401
|
|
447
|
+
// is then stale, and wiping here would silently log out a healthy session
|
|
448
|
+
// (the rotation-race wipe, QA 6873 site 2). Skip the wipe + drop terminal if changed.
|
|
449
|
+
const postRefresh = await getSession(sessionToken);
|
|
450
|
+
const tokenUnchanged =
|
|
451
|
+
!!originalRefreshToken && postRefresh?.idpRefreshToken === originalRefreshToken;
|
|
452
|
+
if (tokenUnchanged) {
|
|
453
|
+
console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: IDP signaled discard_token', {
|
|
454
|
+
reason: 'IDP_DISCARD_TOKEN',
|
|
455
|
+
errorCode,
|
|
456
|
+
userId,
|
|
457
|
+
sessionToken: sessionToken.substring(0, 8) + '...',
|
|
458
|
+
hadRefreshToken: !!currentSession.idpRefreshToken,
|
|
459
|
+
stack: new Error().stack
|
|
460
|
+
});
|
|
461
|
+
await updateSession(sessionToken, {
|
|
462
|
+
idpRefreshToken: '',
|
|
463
|
+
idpRefreshTokenExpires: undefined,
|
|
464
|
+
refreshTokenClearedAt: Date.now(),
|
|
465
|
+
refreshTokenClearedReason: `IDP_DISCARD_TOKEN:${errorCode}`
|
|
466
|
+
});
|
|
467
|
+
} else {
|
|
468
|
+
// Concurrent refresh already rotated the token — our 401 is stale. Return the
|
|
469
|
+
// freshly-rotated token directly so the loser succeeds without a retry
|
|
470
|
+
// (race-fix follow-up #1).
|
|
471
|
+
discardToken = false;
|
|
472
|
+
console.warn('[AUTH_REFRESH] Discard suppressed: refresh token already rotated by a concurrent refresh — returning the fresh token (race-safe).');
|
|
473
|
+
if (postRefresh?.idpAccessToken && postRefresh.idpAccessTokenExpires) {
|
|
474
|
+
return NextResponse.json({
|
|
475
|
+
refreshed: true,
|
|
476
|
+
reason: 'completed_by_concurrent_request',
|
|
477
|
+
accessTokenExpires: postRefresh.idpAccessTokenExpires,
|
|
478
|
+
hasRefreshToken: !!postRefresh.idpRefreshToken,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
459
482
|
}
|
|
460
483
|
|
|
461
484
|
return NextResponse.json({
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { IDPClientConfig } from '../lib/idp-client-config';
|
|
|
18
18
|
import { getIDPClientConfig } from '../lib/idp-client-config';
|
|
19
19
|
import { getAppSlug } from '../lib/app-slug';
|
|
20
20
|
import { getRedis } from '../lib/redis';
|
|
21
|
+
import { SESSION_TTL } from '../lib/session-store';
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Better Auth social provider config shape.
|
|
@@ -133,7 +134,7 @@ export function createBetterAuthInstance(
|
|
|
133
134
|
if (ttl) {
|
|
134
135
|
await redis.setex(`ba:${appSlug}:${key}`, ttl, value);
|
|
135
136
|
} else {
|
|
136
|
-
await redis.setex(`ba:${appSlug}:${key}`,
|
|
137
|
+
await redis.setex(`ba:${appSlug}:${key}`, SESSION_TTL, value);
|
|
137
138
|
}
|
|
138
139
|
} catch { /* Redis unavailable — cookie cache still works */ }
|
|
139
140
|
},
|
|
@@ -364,14 +365,14 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
364
365
|
|
|
365
366
|
// Store in BA Redis session (for decodeSession)
|
|
366
367
|
baData.idpTokens = idpTokenData;
|
|
367
|
-
await getRedis().setex(baKey,
|
|
368
|
+
await getRedis().setex(baKey, SESSION_TTL, JSON.stringify(baData));
|
|
368
369
|
|
|
369
370
|
// Write to canonical session store so refresh handler and token lifecycle can find the tokens.
|
|
370
371
|
// Key format: {sessionPrefix}{token} — same key that getSession() reads from.
|
|
371
372
|
try {
|
|
372
373
|
const { getSessionPrefix } = await import('../lib/app-slug');
|
|
373
374
|
const canonicalKey = `${getSessionPrefix()}${sessionToken}`;
|
|
374
|
-
await getRedis().setex(canonicalKey,
|
|
375
|
+
await getRedis().setex(canonicalKey, SESSION_TTL, JSON.stringify({
|
|
375
376
|
...idpTokenData,
|
|
376
377
|
oauthProvider: provider,
|
|
377
378
|
}));
|