@payez/next-mvp 4.10.0 → 4.12.0
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/CHANGELOG.md +44 -0
- package/dist/api-handlers/account/recovery/forward.js +2 -1
- package/dist/api-handlers/account/recovery/initiate.js +2 -1
- package/dist/api-handlers/account/reset-password.js +2 -1
- package/dist/api-handlers/account/send-code.js +2 -1
- package/dist/api-handlers/account/update-phone.js +2 -1
- package/dist/api-handlers/account/validate-password.js +2 -1
- package/dist/api-handlers/account/verify-email.js +2 -1
- package/dist/api-handlers/account/verify-sms.js +2 -1
- package/dist/api-handlers/anon/preferences.js +3 -2
- package/dist/api-handlers/auth/credential-login-email-code.js +3 -2
- package/dist/api-handlers/auth/credential-login.js +6 -5
- package/dist/api-handlers/auth/refresh.d.ts +17 -58
- package/dist/api-handlers/auth/refresh.js +73 -773
- package/dist/api-handlers/auth/signout.js +43 -6
- package/dist/api-handlers/mfa/backup-code-verify.js +2 -1
- package/dist/api-handlers/mfa/totp-verify.js +2 -1
- package/dist/api-handlers/session/refresh-viability.js +3 -2
- package/dist/api-handlers/session/viability.js +2 -1
- package/dist/auth/better-auth.js +7 -6
- package/dist/auth/idp-oauth.js +4 -2
- package/dist/auth/utils/idp-client.js +5 -4
- package/dist/auth/utils/token-utils.d.ts +1 -1
- package/dist/auth/utils/token-utils.js +7 -5
- package/dist/config/logger.js +2 -1
- package/dist/config/logging-config.js +4 -3
- package/dist/config/vibe-log-transport.js +2 -1
- package/dist/lib/api-handler.js +4 -3
- package/dist/lib/ensure-fresh-access-token.d.ts +24 -1
- package/dist/lib/ensure-fresh-access-token.js +195 -28
- package/dist/lib/geolocation.js +5 -22
- package/dist/lib/idp-client-config.js +4 -3
- package/dist/lib/idp-fetch.d.ts +11 -0
- package/dist/lib/idp-fetch.js +6 -3
- package/dist/lib/idp-logout.d.ts +10 -0
- package/dist/lib/idp-logout.js +73 -1
- package/dist/lib/jwt-decode-client.js +2 -1
- package/dist/lib/jwt-decode.js +2 -1
- package/dist/lib/log-safe.d.ts +19 -0
- package/dist/lib/log-safe.js +32 -0
- package/dist/lib/redis.js +2 -1
- package/dist/lib/refresh-in-doubt.d.ts +31 -0
- package/dist/lib/refresh-in-doubt.js +81 -0
- package/dist/lib/refresh-token-validator.js +2 -1
- package/dist/lib/refresh-window.d.ts +16 -0
- package/dist/lib/refresh-window.js +19 -0
- package/dist/lib/safe-error-response.d.ts +5 -1
- package/dist/lib/safe-error-response.js +7 -2
- package/dist/lib/safe-trace-id.d.ts +11 -0
- package/dist/lib/safe-trace-id.js +23 -0
- package/dist/lib/session-store.js +16 -5
- package/dist/lib/site-logger.d.ts +2 -8
- package/dist/lib/site-logger.js +11 -5
- package/dist/lib/standardized-client-api.js +3 -2
- package/dist/lib/token-lifecycle.js +6 -4
- package/dist/middleware/create-middleware.js +2 -1
- package/dist/middleware/rbac-check.d.ts +0 -13
- package/dist/middleware/rbac-check.js +6 -5
- package/dist/models/SessionModel.d.ts +7 -0
- package/dist/routes/auth/logout.js +6 -2
- package/dist/routes/auth/refresh.d.ts +1 -15
- package/dist/routes/auth/settings.js +2 -1
- package/dist/server/auth-guard.js +4 -3
- package/dist/server/auth.d.ts +1 -1
- package/dist/server/auth.js +5 -9
- package/dist/server/decode-session.js +3 -2
- package/dist/server/with-auth.js +2 -1
- package/dist/stores/authStore.js +3 -2
- package/package.json +1 -1
- package/scripts/verify-release-provenance.mjs +1 -0
- package/src/api-handlers/account/recovery/forward.ts +2 -1
- package/src/api-handlers/account/recovery/initiate.ts +2 -1
- package/src/api-handlers/account/reset-password.ts +2 -1
- package/src/api-handlers/account/send-code.ts +2 -1
- package/src/api-handlers/account/update-phone.ts +2 -1
- package/src/api-handlers/account/validate-password.ts +2 -1
- package/src/api-handlers/account/verify-email.ts +2 -1
- package/src/api-handlers/account/verify-sms.ts +2 -1
- package/src/api-handlers/anon/preferences.ts +3 -2
- package/src/api-handlers/auth/credential-login-email-code.ts +3 -2
- package/src/api-handlers/auth/credential-login.ts +6 -5
- package/src/api-handlers/auth/refresh.ts +74 -813
- package/src/api-handlers/auth/signout.ts +54 -7
- package/src/api-handlers/mfa/backup-code-verify.ts +2 -1
- package/src/api-handlers/mfa/totp-verify.ts +2 -1
- package/src/api-handlers/session/refresh-viability.ts +3 -2
- package/src/api-handlers/session/viability.ts +2 -1
- package/src/auth/better-auth.ts +7 -6
- package/src/auth/idp-oauth.ts +7 -2
- package/src/auth/utils/idp-client.ts +5 -4
- package/src/auth/utils/token-utils.ts +7 -5
- package/src/config/logger.ts +2 -1
- package/src/config/logging-config.ts +4 -3
- package/src/config/vibe-log-transport.ts +2 -1
- package/src/lib/api-handler.ts +4 -3
- package/src/lib/ensure-fresh-access-token.ts +214 -29
- package/src/lib/geolocation.ts +5 -4
- package/src/lib/idp-client-config.ts +4 -3
- package/src/lib/idp-fetch.ts +14 -3
- package/src/lib/idp-logout.ts +36 -1
- package/src/lib/jwt-decode-client.ts +3 -1
- package/src/lib/jwt-decode.ts +2 -1
- package/src/lib/log-safe.ts +30 -0
- package/src/lib/redis.ts +2 -1
- package/src/lib/refresh-in-doubt.ts +79 -0
- package/src/lib/refresh-token-validator.ts +2 -1
- package/src/lib/refresh-window.ts +16 -0
- package/src/lib/safe-error-response.ts +7 -2
- package/src/lib/safe-trace-id.ts +21 -0
- package/src/lib/session-store.ts +17 -4
- package/src/lib/site-logger.ts +13 -7
- package/src/lib/standardized-client-api.ts +3 -2
- package/src/lib/token-lifecycle.ts +6 -4
- package/src/middleware/create-middleware.ts +2 -1
- package/src/middleware/rbac-check.ts +2 -1
- package/src/models/SessionModel.ts +7 -0
- package/src/routes/auth/logout.ts +9 -2
- package/src/routes/auth/settings.ts +2 -1
- package/src/server/auth-guard.ts +4 -3
- package/src/server/auth.ts +6 -11
- package/src/server/decode-session.ts +3 -2
- package/src/server/with-auth.ts +2 -1
- package/src/stores/authStore.ts +3 -2
|
@@ -11,7 +11,15 @@
|
|
|
11
11
|
|
|
12
12
|
import { NextRequest, NextResponse } from 'next/server';
|
|
13
13
|
import { cookies } from 'next/headers';
|
|
14
|
-
import { deleteSessionEverywhere } from '../../lib/session-store';
|
|
14
|
+
import { deleteSessionEverywhere, getSession as getStoredSession, getBetterAuthSession as getStoredBetterAuthSession } from '../../lib/session-store';
|
|
15
|
+
import { revokeSessionAtIdp } from '../../lib/idp-logout';
|
|
16
|
+
import { errForLog } from '../../lib/log-safe';
|
|
17
|
+
import {
|
|
18
|
+
SESSION_NOT_ENDED_CODE,
|
|
19
|
+
SESSION_NOT_ENDED_IDP_MESSAGE,
|
|
20
|
+
SESSION_NOT_ENDED_LOCAL_MESSAGE,
|
|
21
|
+
type SessionNotEndedCause,
|
|
22
|
+
} from '../../routes/auth/logout';
|
|
15
23
|
import { getSession } from '../../server/auth';
|
|
16
24
|
import {
|
|
17
25
|
getSessionCookieName,
|
|
@@ -147,23 +155,55 @@ export function createSignoutHandler() {
|
|
|
147
155
|
}
|
|
148
156
|
}
|
|
149
157
|
|
|
150
|
-
//
|
|
151
|
-
//
|
|
158
|
+
// GOV-023 (BAPert 76687): ending the session ends the IdP GRANT first, the same IdP-first path as routes/auth/logout. Before this, this
|
|
159
|
+
// handler only deleted the local record, so a consumer who mounted it left the IdP refresh token live until it expired.
|
|
160
|
+
// A failed or unresolved revoke is the same typed failure as a failed local delete (SESSION_NOT_ENDED + cause); the local delete still
|
|
161
|
+
// runs and the cookies are cleared either way.
|
|
162
|
+
let idpRevokeFailed = false;
|
|
163
|
+
let localDeleteFailed = false;
|
|
152
164
|
if (redisSessionToken) {
|
|
165
|
+
// Does the stored session hold an IdP refresh token? (revokeSessionAtIdp reads the canonical row, then the Better Auth record.)
|
|
166
|
+
let heldRefreshToken = false;
|
|
167
|
+
try {
|
|
168
|
+
const stored = (await getStoredSession(redisSessionToken)) ?? (await getStoredBetterAuthSession(redisSessionToken));
|
|
169
|
+
heldRefreshToken = !!stored?.idpRefreshToken;
|
|
170
|
+
} catch {
|
|
171
|
+
// unknown: treated as "none held", so an unreadable store does not turn a clean sign-out into a failure on its own
|
|
172
|
+
}
|
|
173
|
+
try {
|
|
174
|
+
const outcome = await revokeSessionAtIdp(req);
|
|
175
|
+
// 'nothing-to-revoke' while a refresh token IS held means the helper could not resolve this session from the request (this
|
|
176
|
+
// handler also finds the session by decoding the cookie when Better Auth cannot): the grant is NOT ended, so say so.
|
|
177
|
+
if (outcome === 'nothing-to-revoke' && heldRefreshToken) {
|
|
178
|
+
idpRevokeFailed = true;
|
|
179
|
+
console.error('[SIGNOUT] IdP revoke could not resolve the session - the IdP refresh token is STILL LIVE');
|
|
180
|
+
}
|
|
181
|
+
} catch (revokeError) {
|
|
182
|
+
idpRevokeFailed = true;
|
|
183
|
+
console.error(
|
|
184
|
+
(revokeError as { grantLive?: boolean } | null)?.grantLive === true
|
|
185
|
+
? '[SIGNOUT] IdP revoke failed - the IdP refresh token is STILL LIVE:'
|
|
186
|
+
: '[SIGNOUT] IdP revoke could not be confirmed (the grant may already be dead):',
|
|
187
|
+
errForLog(revokeError),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Delete the session from EVERY store (181922): canonical + BA secondary.
|
|
192
|
+
// Canonical-only deletion let the ba: record resurrect the session.
|
|
153
193
|
try {
|
|
154
194
|
await deleteSessionEverywhere(redisSessionToken);
|
|
155
195
|
console.info('[SIGNOUT] Redis session cleanup successful', {
|
|
156
196
|
sessionToken: tokenLogId(redisSessionToken)
|
|
157
197
|
});
|
|
158
198
|
} catch (sessionError) {
|
|
159
|
-
|
|
160
|
-
console.error('[SIGNOUT] Redis session cleanup failed
|
|
199
|
+
localDeleteFailed = true;
|
|
200
|
+
console.error('[SIGNOUT] Redis session cleanup failed - the server session is STILL LIVE:', errForLog(sessionError));
|
|
161
201
|
}
|
|
162
202
|
} else {
|
|
163
203
|
console.warn('[SIGNOUT] No Redis session token (UUID) extracted from cookie; skipping Redis deletion');
|
|
164
204
|
}
|
|
165
205
|
|
|
166
|
-
// Build response
|
|
206
|
+
// Build response. A sign-out that did not end the session must not say it did (same typed answer as routes/auth/logout).
|
|
167
207
|
const responseData: SignoutResponse = {
|
|
168
208
|
success: true,
|
|
169
209
|
message: sessionToken ? 'Session deleted successfully' : 'No active session found',
|
|
@@ -171,7 +211,14 @@ export function createSignoutHandler() {
|
|
|
171
211
|
chunkCookiesDeleted: chunkCookies.length
|
|
172
212
|
};
|
|
173
213
|
|
|
174
|
-
const response =
|
|
214
|
+
const response = (localDeleteFailed || idpRevokeFailed)
|
|
215
|
+
? NextResponse.json(
|
|
216
|
+
localDeleteFailed
|
|
217
|
+
? { success: false, code: SESSION_NOT_ENDED_CODE, cause: 'local_delete_failed' as SessionNotEndedCause, message: SESSION_NOT_ENDED_LOCAL_MESSAGE }
|
|
218
|
+
: { success: false, code: SESSION_NOT_ENDED_CODE, cause: 'idp_unconfirmed' as SessionNotEndedCause, message: SESSION_NOT_ENDED_IDP_MESSAGE },
|
|
219
|
+
{ status: 500 },
|
|
220
|
+
)
|
|
221
|
+
: NextResponse.json(responseData);
|
|
175
222
|
|
|
176
223
|
// Always clear cookies, regardless of success/failure (using app-slug prefixed names)
|
|
177
224
|
try {
|
|
@@ -60,6 +60,7 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
|
60
60
|
import { transitionTo2FASession } from '../../lib/session-store';
|
|
61
61
|
import { jwtDecode } from '../../lib/jwt-decode';
|
|
62
62
|
import { twoFactorVerifyFailure } from '../../lib/two-factor-verify-failure';
|
|
63
|
+
import { errForLog } from '../../lib/log-safe';
|
|
63
64
|
|
|
64
65
|
export async function POST(req: NextRequest) {
|
|
65
66
|
try {
|
|
@@ -157,7 +158,7 @@ export async function POST(req: NextRequest) {
|
|
|
157
158
|
{ status: 200 }
|
|
158
159
|
);
|
|
159
160
|
} catch (error) {
|
|
160
|
-
console.error('[VERIFY_BACKUP_CODE] Error:', error);
|
|
161
|
+
console.error('[VERIFY_BACKUP_CODE] Error:', errForLog(error));
|
|
161
162
|
return NextResponse.json(
|
|
162
163
|
{
|
|
163
164
|
success: false,
|
|
@@ -35,6 +35,7 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
|
35
35
|
import { transitionTo2FASession } from '../../lib/session-store';
|
|
36
36
|
import { jwtDecode } from '../../lib/jwt-decode';
|
|
37
37
|
import { twoFactorVerifyFailure } from '../../lib/two-factor-verify-failure';
|
|
38
|
+
import { errForLog } from '../../lib/log-safe';
|
|
38
39
|
|
|
39
40
|
export async function POST(req: NextRequest) {
|
|
40
41
|
try {
|
|
@@ -159,7 +160,7 @@ export async function POST(req: NextRequest) {
|
|
|
159
160
|
{ status: 200 }
|
|
160
161
|
);
|
|
161
162
|
} catch (error) {
|
|
162
|
-
console.error('[VERIFY_TOTP] Error:', error);
|
|
163
|
+
console.error('[VERIFY_TOTP] Error:', errForLog(error));
|
|
163
164
|
return NextResponse.json(
|
|
164
165
|
{
|
|
165
166
|
success: false,
|
|
@@ -4,6 +4,7 @@ import { checkRefreshViability } from '../../lib/refresh-token-validator';
|
|
|
4
4
|
import { getTokenTestAware } from '../../lib/test-aware-get-token';
|
|
5
5
|
import { logger } from '../../config/logger';
|
|
6
6
|
import { tokenLogId } from '../../lib/token-log-id';
|
|
7
|
+
import { errForLog } from '../../lib/log-safe';
|
|
7
8
|
import { resolveSessionState } from '../../server/auth';
|
|
8
9
|
|
|
9
10
|
export async function GET(req: NextRequest) {
|
|
@@ -49,8 +50,8 @@ export async function GET(req: NextRequest) {
|
|
|
49
50
|
return NextResponse.json({ canRefresh: viabilityCheck.canRefresh, reason: viabilityCheck.reason, timeRemaining: viabilityCheck.timeRemaining, expiresAt: viabilityCheck.expiresAt, accessTokenExpired: viabilityCheck.accessTokenExpired, accessTokenTimeRemaining: viabilityCheck.accessTokenTimeRemaining, sessionTokenId: tokenLogId(finalSessionToken), twoFactorComplete: sessionData.mfaVerified ?? false, userId: sessionData.userId, refreshInProgress: false });
|
|
50
51
|
} catch (error) {
|
|
51
52
|
const correlationId = globalThis.crypto.randomUUID();
|
|
52
|
-
logger.error('[REFRESH-VIABILITY] Error checking refresh viability', { correlationId, error:
|
|
53
|
-
// PAY-1438: the
|
|
53
|
+
logger.error('[REFRESH-VIABILITY] Error checking refresh viability', { correlationId, error: errForLog(error) });
|
|
54
|
+
// PAY-1438: the caller gets the id; the log line names the error only (errForLog), never its message (PII-in-logs group 1, BAPert 76553).
|
|
54
55
|
return NextResponse.json({ canRefresh: false, reason: 'check_error', error: 'Failed to check refresh viability', code: 'INTERNAL_ERROR', correlationId }, { status: 500 });
|
|
55
56
|
}
|
|
56
57
|
}
|
|
@@ -12,6 +12,7 @@ import { isInitializationFailed, ensureInitialized } from '../../lib/startup-ini
|
|
|
12
12
|
import { getIDPClientConfig } from '../../lib/idp-client-config';
|
|
13
13
|
import { tokenLogId } from '../../lib/token-log-id';
|
|
14
14
|
import { getRevocationStatus, warnNonNumericUserId, resolveNumericIdpUserId, type RevocationStatus } from '../../vibe/sessions';
|
|
15
|
+
import { errForLog } from '../../lib/log-safe';
|
|
15
16
|
|
|
16
17
|
export async function GET(req: NextRequest) {
|
|
17
18
|
try {
|
|
@@ -233,7 +234,7 @@ export async function GET(req: NextRequest) {
|
|
|
233
234
|
return NextResponse.json({ authenticated: false });
|
|
234
235
|
|
|
235
236
|
} catch (error) {
|
|
236
|
-
console.error('[API Viability] Error checking session viability:', error);
|
|
237
|
+
console.error('[API Viability] Error checking session viability:', errForLog(error));
|
|
237
238
|
return NextResponse.json({ authenticated: false }, { status: 500 });
|
|
238
239
|
}
|
|
239
240
|
}
|
package/src/auth/better-auth.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { getRedis } from '../lib/redis';
|
|
|
33
33
|
import { createSingleFlight, type SingleFlight } from '../lib/single-flight';
|
|
34
34
|
import { SESSION_TTL } from '../lib/session-store';
|
|
35
35
|
import { tokenLogId } from '../lib/token-log-id';
|
|
36
|
+
import { errForLog } from '../lib/log-safe';
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* 185121: short sha256 fingerprint for secrets/tokens in log lines — enough
|
|
@@ -855,7 +856,7 @@ export function createBetterAuthInstance(
|
|
|
855
856
|
const { deleteSession } = await import('../lib/session-store');
|
|
856
857
|
await deleteSession(key);
|
|
857
858
|
} catch (err) {
|
|
858
|
-
console.warn('[BETTER_AUTH] secondary delete: canonical store cleanup failed:', err);
|
|
859
|
+
console.warn('[BETTER_AUTH] secondary delete: canonical store cleanup failed:', errForLog(err));
|
|
859
860
|
}
|
|
860
861
|
},
|
|
861
862
|
},
|
|
@@ -1356,7 +1357,7 @@ export async function exchangeOAuthForIdpTokens(
|
|
|
1356
1357
|
// true = success (tokens stored). false = a non-IdP failure with no specific code (generic abort).
|
|
1357
1358
|
return stored;
|
|
1358
1359
|
} catch (err) {
|
|
1359
|
-
console.error('[BETTER_AUTH] IDP token exchange failed:',
|
|
1360
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', errForLog(err));
|
|
1360
1361
|
return false;
|
|
1361
1362
|
}
|
|
1362
1363
|
}
|
|
@@ -1446,7 +1447,7 @@ export async function storeIdpTokensInSession(
|
|
|
1446
1447
|
|
|
1447
1448
|
return true;
|
|
1448
1449
|
} catch (err) {
|
|
1449
|
-
console.error('[BETTER_AUTH] storeIdpTokensInSession failed:',
|
|
1450
|
+
console.error('[BETTER_AUTH] storeIdpTokensInSession failed:', errForLog(err));
|
|
1450
1451
|
return false;
|
|
1451
1452
|
}
|
|
1452
1453
|
}
|
|
@@ -1547,13 +1548,13 @@ async function abortOAuthSignIn(
|
|
|
1547
1548
|
const out = await auth?.api?.signOut?.({ headers: new Headers({ cookie: cookieHeader }), asResponse: true });
|
|
1548
1549
|
for (const sc of (out as Response | undefined)?.headers?.getSetCookie?.() ?? []) headers.append('set-cookie', sc);
|
|
1549
1550
|
} catch (err) {
|
|
1550
|
-
console.warn('[BETTER_AUTH] abort: Better Auth signOut failed, deleting the record directly:',
|
|
1551
|
+
console.warn('[BETTER_AUTH] abort: Better Auth signOut failed, deleting the record directly:', errForLog(err));
|
|
1551
1552
|
}
|
|
1552
1553
|
try {
|
|
1553
1554
|
const config = await getIDPClientConfig();
|
|
1554
1555
|
await getRedis().del(`ba:${config.clientSlug || getAppSlug()}:${sessionToken}`);
|
|
1555
1556
|
} catch (err) {
|
|
1556
|
-
console.error('[BETTER_AUTH] abort: could not delete the Better Auth session record', tokenLogId(sessionToken),
|
|
1557
|
+
console.error('[BETTER_AUTH] abort: could not delete the Better Auth session record', tokenLogId(sessionToken), errForLog(err));
|
|
1557
1558
|
}
|
|
1558
1559
|
}
|
|
1559
1560
|
for (const sc of expireSetCookies(callbackResponse.headers.getSetCookie?.() ?? [])) headers.append('set-cookie', sc);
|
|
@@ -1673,7 +1674,7 @@ export function createAuthGetHandler(loginPath: string = '/account-auth/login')
|
|
|
1673
1674
|
}
|
|
1674
1675
|
}
|
|
1675
1676
|
} catch (err: any) {
|
|
1676
|
-
console.error('[BETTER_AUTH] IDP token exchange failed:', err
|
|
1677
|
+
console.error('[BETTER_AUTH] IDP token exchange failed:', errForLog(err));
|
|
1677
1678
|
}
|
|
1678
1679
|
if (exchange !== true) {
|
|
1679
1680
|
// A specific IdP error code goes on the URL; otherwise the generic OAUTH_SIGN_IN_FAILED.
|
package/src/auth/idp-oauth.ts
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
* case-insensitively, at the authorize step). Both come from ONE value: the `redirectURI` of the provider config.
|
|
20
20
|
*/
|
|
21
21
|
import { createRemoteJWKSet, decodeProtectedHeader, jwtVerify, type JWTVerifyGetKey } from 'jose';
|
|
22
|
+
import { errForLog } from '../lib/log-safe';
|
|
22
23
|
|
|
23
24
|
export interface IdpTokenGrant {
|
|
24
25
|
accessToken: string;
|
|
@@ -281,7 +282,11 @@ export function createIdpOAuthHooks(opts: IdpOAuthHookOptions) {
|
|
|
281
282
|
if (responseMasked) console.warn('[BETTER_AUTH] payez-idp token response address is masked or unusable; ignored');
|
|
282
283
|
const email = resolveEmail(payload as Record<string, unknown>, responseEmailUsable ? responseEmailRaw : undefined);
|
|
283
284
|
if (!email) {
|
|
284
|
-
|
|
285
|
+
// A closed `code` rides on the error so the refusal log tells an operator WHY (errForLog prints a closed UPPER_SNAKE code) with no value (BAPert 76998 E11).
|
|
286
|
+
throw Object.assign(
|
|
287
|
+
new Error(responseMasked ? 'response address is masked' : 'no usable email (needs data.user.email or an email claim, address-shaped, equal when both are present)'),
|
|
288
|
+
{ code: responseMasked ? 'RESPONSE_ADDRESS_MASKED' : 'NO_USABLE_EMAIL' },
|
|
289
|
+
);
|
|
285
290
|
}
|
|
286
291
|
const name =
|
|
287
292
|
(typeof payload.name === 'string' && payload.name.trim()) ||
|
|
@@ -301,7 +306,7 @@ export function createIdpOAuthHooks(opts: IdpOAuthHookOptions) {
|
|
|
301
306
|
return { id: claims.sub, email: claims.email, name: claims.name, emailVerified: claims.emailVerified };
|
|
302
307
|
} catch (err) {
|
|
303
308
|
// Reason only, never the token.
|
|
304
|
-
console.warn('[BETTER_AUTH] payez-idp access token refused:',
|
|
309
|
+
console.warn('[BETTER_AUTH] payez-idp access token refused:', errForLog(err));
|
|
305
310
|
return null;
|
|
306
311
|
}
|
|
307
312
|
}
|
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
IdpRefreshResponse,
|
|
19
19
|
LoginCredentials,
|
|
20
20
|
} from '../types/auth-types';
|
|
21
|
+
import { errForLog } from '../../lib/log-safe';
|
|
21
22
|
|
|
22
23
|
// ============================================================================
|
|
23
24
|
// CONFIGURATION
|
|
@@ -109,7 +110,7 @@ export async function idpLogin(
|
|
|
109
110
|
result: responseData.result,
|
|
110
111
|
};
|
|
111
112
|
} catch (error) {
|
|
112
|
-
console.error('[IDP_CLIENT] Login request failed:', error);
|
|
113
|
+
console.error('[IDP_CLIENT] Login request failed:', errForLog(error));
|
|
113
114
|
return {
|
|
114
115
|
success: false,
|
|
115
116
|
error: {
|
|
@@ -246,7 +247,7 @@ export async function idpRefreshToken(
|
|
|
246
247
|
},
|
|
247
248
|
};
|
|
248
249
|
} catch (error) {
|
|
249
|
-
console.error('[IDP_CLIENT] Token refresh request failed:', error);
|
|
250
|
+
console.error('[IDP_CLIENT] Token refresh request failed:', errForLog(error));
|
|
250
251
|
return {
|
|
251
252
|
success: false,
|
|
252
253
|
error: {
|
|
@@ -304,7 +305,7 @@ export async function idpVerify2FA(
|
|
|
304
305
|
|
|
305
306
|
return { success: true };
|
|
306
307
|
} catch (error) {
|
|
307
|
-
console.error('[IDP_CLIENT] 2FA verification failed:', error);
|
|
308
|
+
console.error('[IDP_CLIENT] 2FA verification failed:', errForLog(error));
|
|
308
309
|
return {
|
|
309
310
|
success: false,
|
|
310
311
|
error: {
|
|
@@ -352,7 +353,7 @@ export async function idpSend2FACode(
|
|
|
352
353
|
|
|
353
354
|
return { success: true };
|
|
354
355
|
} catch (error) {
|
|
355
|
-
console.error('[IDP_CLIENT] Send 2FA code failed:', error);
|
|
356
|
+
console.error('[IDP_CLIENT] Send 2FA code failed:', errForLog(error));
|
|
356
357
|
return {
|
|
357
358
|
success: false,
|
|
358
359
|
error: {
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
import { jwtDecode, decodeJwtHeader, extractKidFromToken, JwtHeader } from '../../lib/jwt-decode';
|
|
11
11
|
import type { DecodedIdpAccessToken } from '../types/auth-types';
|
|
12
|
+
import { REFRESH_WINDOW_MS } from '../../lib/refresh-window';
|
|
13
|
+
import { errForLog } from '../../lib/log-safe';
|
|
12
14
|
|
|
13
15
|
// Re-export header utilities for consumers
|
|
14
16
|
export { decodeJwtHeader, extractKidFromToken, type JwtHeader } from '../../lib/jwt-decode';
|
|
@@ -27,7 +29,7 @@ export function decodeIdpAccessToken(token: string): DecodedIdpAccessToken | nul
|
|
|
27
29
|
try {
|
|
28
30
|
return jwtDecode<DecodedIdpAccessToken>(token);
|
|
29
31
|
} catch (error) {
|
|
30
|
-
console.error('[TOKEN_UTILS] Failed to decode access token:', error);
|
|
32
|
+
console.error('[TOKEN_UTILS] Failed to decode access token:', errForLog(error));
|
|
31
33
|
return null;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
@@ -58,7 +60,7 @@ export function decodeIdpAccessTokenFull(token: string): {
|
|
|
58
60
|
bearerKeyId: header.kid,
|
|
59
61
|
};
|
|
60
62
|
} catch (error) {
|
|
61
|
-
console.error('[TOKEN_UTILS] Failed to decode access token (full):', error);
|
|
63
|
+
console.error('[TOKEN_UTILS] Failed to decode access token (full):', errForLog(error));
|
|
62
64
|
return null;
|
|
63
65
|
}
|
|
64
66
|
}
|
|
@@ -137,12 +139,12 @@ export function extractAmrFromToken(decoded: DecodedIdpAccessToken): string[] {
|
|
|
137
139
|
* Check if a token expiry timestamp indicates the token needs refresh.
|
|
138
140
|
*
|
|
139
141
|
* @param expiresAt - Token expiry timestamp (Unix milliseconds)
|
|
140
|
-
* @param bufferMs - How early to trigger refresh (default 5 minutes)
|
|
142
|
+
* @param bufferMs - How early to trigger refresh (default: the one refresh window, `REFRESH_WINDOW_MS`, 5 minutes)
|
|
141
143
|
* @returns true if token is expired or will expire within buffer period
|
|
142
144
|
*/
|
|
143
145
|
export function tokenNeedsRefresh(
|
|
144
146
|
expiresAt: number | undefined,
|
|
145
|
-
bufferMs: number =
|
|
147
|
+
bufferMs: number = REFRESH_WINDOW_MS
|
|
146
148
|
): boolean {
|
|
147
149
|
if (!expiresAt) {
|
|
148
150
|
return true; // No expiry info = assume needs refresh
|
|
@@ -237,7 +239,7 @@ export function validateTokenExpiry(
|
|
|
237
239
|
mismatch,
|
|
238
240
|
};
|
|
239
241
|
} catch (error) {
|
|
240
|
-
console.error('[TOKEN_UTILS] Failed to validate token expiry:', error);
|
|
242
|
+
console.error('[TOKEN_UTILS] Failed to validate token expiry:', errForLog(error));
|
|
241
243
|
return { valid: false, actualExpiresAt: null, mismatch: false };
|
|
242
244
|
}
|
|
243
245
|
}
|
package/src/config/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getLoggingConfigSync, startConfigWatching, addConfigWatcher } from './logging-config';
|
|
2
2
|
import { LoggingConfig } from '../types/logging';
|
|
3
3
|
import { createVibeLogTransport } from './vibe-log-transport';
|
|
4
|
+
import { errForLog } from '../lib/log-safe';
|
|
4
5
|
|
|
5
6
|
let loggingConfig: LoggingConfig = getLoggingConfigSync();
|
|
6
7
|
startConfigWatching();
|
|
@@ -41,7 +42,7 @@ const createLogger = (): any => {
|
|
|
41
42
|
console.log('[LOGGER] Vibe Redis transport DISABLED - no REDIS_URL configured');
|
|
42
43
|
}
|
|
43
44
|
return winston.createLogger({ level: loggingConfig.logLevel, levels: loggingConfig.levels, format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()), transports, exitOnError: false });
|
|
44
|
-
} catch (error) { console.warn('Failed to initialize Winston logger, falling back to EdgeLogger:', error); return new EdgeLogger(); }
|
|
45
|
+
} catch (error) { console.warn('Failed to initialize Winston logger, falling back to EdgeLogger:', errForLog(error)); return new EdgeLogger(); }
|
|
45
46
|
};
|
|
46
47
|
|
|
47
48
|
let logger: any;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { promises as fs } from 'fs';
|
|
2
2
|
import { watch, existsSync } from 'fs';
|
|
3
3
|
import { LoggingConfig, LogLevel } from '../types/logging';
|
|
4
|
+
import { errForLog } from '../lib/log-safe';
|
|
4
5
|
|
|
5
6
|
const isEdgeRuntime = typeof (globalThis as any).EdgeRuntime !== 'undefined' || process.env.NEXT_RUNTIME === 'edge';
|
|
6
7
|
|
|
@@ -35,7 +36,7 @@ class LoggingConfigManager {
|
|
|
35
36
|
const config = this.validateAndMergeConfig(rawConfig);
|
|
36
37
|
this.config = config; this.notifyWatchers(config); return config;
|
|
37
38
|
} catch (error) {
|
|
38
|
-
console.warn(`Failed to load logging config from ${this.configPath}:`, error);
|
|
39
|
+
console.warn(`Failed to load logging config from ${this.configPath}:`, errForLog(error));
|
|
39
40
|
return this.getDefaultConfig();
|
|
40
41
|
}
|
|
41
42
|
}
|
|
@@ -43,11 +44,11 @@ class LoggingConfigManager {
|
|
|
43
44
|
async getConfig(): Promise<LoggingConfig> { if (!this.config) { return await this.loadConfig(); } return this.config; }
|
|
44
45
|
getConfigSync(): LoggingConfig { return this.config || this.getDefaultConfig(); }
|
|
45
46
|
|
|
46
|
-
startWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { return; } try { if (!existsSync(this.configPath)) { console.warn(`Logging config file not found at ${this.configPath} — watch disabled`); return; } this.fileWatcher = watch(this.configPath, { persistent: false }, (eventType) => { if (eventType === 'change') { this.loadConfig().catch(console.error); } }); } catch (error) { console.warn('Failed to start watching logging config file:', error); } }
|
|
47
|
+
startWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { return; } try { if (!existsSync(this.configPath)) { console.warn(`Logging config file not found at ${this.configPath} — watch disabled`); return; } this.fileWatcher = watch(this.configPath, { persistent: false }, (eventType) => { if (eventType === 'change') { this.loadConfig().catch(console.error); } }); } catch (error) { console.warn('Failed to start watching logging config file:', errForLog(error)); } }
|
|
47
48
|
stopWatching(): void { if (this.isEdgeRuntime || this.isBrowser) { return; } if (this.fileWatcher) { this.fileWatcher.close(); this.fileWatcher = null; } }
|
|
48
49
|
addWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.add(callback); }
|
|
49
50
|
removeWatcher(callback: (config: LoggingConfig) => void): void { this.watchers.delete(callback); }
|
|
50
|
-
private notifyWatchers(config: LoggingConfig): void { this.watchers.forEach(cb => { try { cb(config); } catch (error) { console.error('Error in logging config watcher:', error); } }); }
|
|
51
|
+
private notifyWatchers(config: LoggingConfig): void { this.watchers.forEach(cb => { try { cb(config); } catch (error) { console.error('Error in logging config watcher:', errForLog(error)); } }); }
|
|
51
52
|
|
|
52
53
|
private validateAndMergeConfig(rawConfig: any): LoggingConfig {
|
|
53
54
|
const defaults = this.getDefaultConfig();
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getRedis } from '../lib/redis';
|
|
15
|
+
import { errForLog } from '../lib/log-safe';
|
|
15
16
|
|
|
16
17
|
// Dynamic import — winston is a peerDependency and may not be installed.
|
|
17
18
|
// We resolve the base class lazily so builds don't break without it.
|
|
@@ -188,7 +189,7 @@ export class VibeLogTransport extends TransportBase {
|
|
|
188
189
|
this.batch = [...entries, ...this.batch].slice(0, 100);
|
|
189
190
|
}
|
|
190
191
|
// Log to console as fallback
|
|
191
|
-
console.error('[VibeLogTransport] Failed to flush logs:', err);
|
|
192
|
+
console.error('[VibeLogTransport] Failed to flush logs:', errForLog(err));
|
|
192
193
|
} finally {
|
|
193
194
|
this.isFlushing = false;
|
|
194
195
|
}
|
package/src/lib/api-handler.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { getJwtCookieName } from './app-slug';
|
|
27
27
|
import { getTokenTestAware } from './test-aware-get-token';
|
|
28
28
|
import { internalRefresh } from './internal-api';
|
|
29
|
+
import { REFRESH_WINDOW_MS } from './refresh-window';
|
|
29
30
|
|
|
30
31
|
// ============================================================================
|
|
31
32
|
// Types
|
|
@@ -284,7 +285,7 @@ export class ApiHandler {
|
|
|
284
285
|
// Skip the optimization (hasRefreshToken check) when access token is missing —
|
|
285
286
|
// some session shapes store the refresh token under different field names,
|
|
286
287
|
// and we still want a refresh attempt to populate the access token.
|
|
287
|
-
const thresholdMs =
|
|
288
|
+
const thresholdMs = REFRESH_WINDOW_MS;
|
|
288
289
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
289
290
|
const expires = sessionData.idpAccessTokenExpires || (sessionData as any).accessTokenExpires || 0;
|
|
290
291
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -426,7 +427,7 @@ export class ApiHandler {
|
|
|
426
427
|
try {
|
|
427
428
|
// Double-check if still needs refresh
|
|
428
429
|
const latest = await getSession(sessionToken);
|
|
429
|
-
const thresholdMs =
|
|
430
|
+
const thresholdMs = REFRESH_WINDOW_MS;
|
|
430
431
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
431
432
|
const latestAny = latest as any;
|
|
432
433
|
// Sessions may store the access token under either `accessToken` or `idpAccessToken`,
|
|
@@ -481,7 +482,7 @@ export class ApiHandler {
|
|
|
481
482
|
const session = await getSession(sessionToken);
|
|
482
483
|
if (session?.accessToken) {
|
|
483
484
|
const expires = session.accessTokenExpires || 0;
|
|
484
|
-
if ((expires - Date.now()) >
|
|
485
|
+
if ((expires - Date.now()) > REFRESH_WINDOW_MS) {
|
|
485
486
|
return { success: true };
|
|
486
487
|
}
|
|
487
488
|
}
|