@payez/next-mvp 4.11.0 → 4.12.1

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.
Files changed (100) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/api-handlers/account/recovery/forward.js +2 -1
  3. package/dist/api-handlers/account/recovery/initiate.js +2 -1
  4. package/dist/api-handlers/account/reset-password.js +2 -1
  5. package/dist/api-handlers/account/send-code.js +2 -1
  6. package/dist/api-handlers/account/update-phone.js +2 -1
  7. package/dist/api-handlers/account/validate-password.js +2 -1
  8. package/dist/api-handlers/account/verify-email.js +2 -1
  9. package/dist/api-handlers/account/verify-sms.js +2 -1
  10. package/dist/api-handlers/anon/preferences.js +3 -2
  11. package/dist/api-handlers/auth/credential-login-email-code.js +3 -2
  12. package/dist/api-handlers/auth/credential-login.js +6 -5
  13. package/dist/api-handlers/auth/refresh.js +2 -1
  14. package/dist/api-handlers/auth/signout.js +43 -6
  15. package/dist/api-handlers/mfa/backup-code-verify.js +2 -1
  16. package/dist/api-handlers/mfa/totp-verify.js +2 -1
  17. package/dist/api-handlers/session/refresh-viability.js +3 -2
  18. package/dist/api-handlers/session/viability.js +2 -1
  19. package/dist/auth/better-auth.js +7 -6
  20. package/dist/auth/idp-oauth.js +4 -2
  21. package/dist/auth/utils/idp-client.js +5 -4
  22. package/dist/auth/utils/token-utils.js +4 -3
  23. package/dist/config/logger.js +2 -1
  24. package/dist/config/logging-config.js +4 -3
  25. package/dist/config/vibe-log-transport.js +2 -1
  26. package/dist/lib/ensure-fresh-access-token.js +17 -11
  27. package/dist/lib/geolocation.js +5 -22
  28. package/dist/lib/idp-client-config.js +4 -3
  29. package/dist/lib/idp-fetch.d.ts +11 -0
  30. package/dist/lib/idp-fetch.js +3 -1
  31. package/dist/lib/idp-logout.d.ts +10 -0
  32. package/dist/lib/idp-logout.js +73 -1
  33. package/dist/lib/jwt-decode-client.js +2 -1
  34. package/dist/lib/jwt-decode.js +2 -1
  35. package/dist/lib/log-safe.d.ts +19 -0
  36. package/dist/lib/log-safe.js +32 -0
  37. package/dist/lib/redis.js +2 -1
  38. package/dist/lib/safe-error-response.d.ts +5 -1
  39. package/dist/lib/safe-error-response.js +7 -2
  40. package/dist/lib/session-store.js +2 -1
  41. package/dist/lib/site-logger.d.ts +2 -8
  42. package/dist/lib/site-logger.js +11 -5
  43. package/dist/lib/standardized-client-api.js +3 -2
  44. package/dist/lib/token-lifecycle.js +3 -2
  45. package/dist/middleware/create-middleware.js +2 -1
  46. package/dist/middleware/rbac-check.d.ts +0 -13
  47. package/dist/middleware/rbac-check.js +6 -5
  48. package/dist/routes/auth/logout.js +6 -2
  49. package/dist/routes/auth/settings.js +2 -1
  50. package/dist/server/auth-guard.js +4 -3
  51. package/dist/server/auth.js +5 -9
  52. package/dist/server/decode-session.js +3 -2
  53. package/dist/server/with-auth.js +2 -1
  54. package/package.json +1 -1
  55. package/src/api-handlers/account/recovery/forward.ts +2 -1
  56. package/src/api-handlers/account/recovery/initiate.ts +2 -1
  57. package/src/api-handlers/account/reset-password.ts +2 -1
  58. package/src/api-handlers/account/send-code.ts +2 -1
  59. package/src/api-handlers/account/update-phone.ts +2 -1
  60. package/src/api-handlers/account/validate-password.ts +2 -1
  61. package/src/api-handlers/account/verify-email.ts +2 -1
  62. package/src/api-handlers/account/verify-sms.ts +2 -1
  63. package/src/api-handlers/anon/preferences.ts +3 -2
  64. package/src/api-handlers/auth/credential-login-email-code.ts +3 -2
  65. package/src/api-handlers/auth/credential-login.ts +6 -5
  66. package/src/api-handlers/auth/refresh.ts +2 -1
  67. package/src/api-handlers/auth/signout.ts +54 -7
  68. package/src/api-handlers/mfa/backup-code-verify.ts +2 -1
  69. package/src/api-handlers/mfa/totp-verify.ts +2 -1
  70. package/src/api-handlers/session/refresh-viability.ts +3 -2
  71. package/src/api-handlers/session/viability.ts +2 -1
  72. package/src/auth/better-auth.ts +7 -6
  73. package/src/auth/idp-oauth.ts +7 -2
  74. package/src/auth/utils/idp-client.ts +5 -4
  75. package/src/auth/utils/token-utils.ts +4 -3
  76. package/src/config/logger.ts +2 -1
  77. package/src/config/logging-config.ts +4 -3
  78. package/src/config/vibe-log-transport.ts +2 -1
  79. package/src/lib/ensure-fresh-access-token.ts +17 -11
  80. package/src/lib/geolocation.ts +5 -4
  81. package/src/lib/idp-client-config.ts +4 -3
  82. package/src/lib/idp-fetch.ts +11 -1
  83. package/src/lib/idp-logout.ts +36 -1
  84. package/src/lib/jwt-decode-client.ts +3 -1
  85. package/src/lib/jwt-decode.ts +2 -1
  86. package/src/lib/log-safe.ts +30 -0
  87. package/src/lib/redis.ts +2 -1
  88. package/src/lib/safe-error-response.ts +7 -2
  89. package/src/lib/session-store.ts +2 -1
  90. package/src/lib/site-logger.ts +13 -7
  91. package/src/lib/standardized-client-api.ts +3 -2
  92. package/src/lib/token-lifecycle.ts +3 -2
  93. package/src/middleware/create-middleware.ts +2 -1
  94. package/src/middleware/rbac-check.ts +2 -1
  95. package/src/routes/auth/logout.ts +9 -2
  96. package/src/routes/auth/settings.ts +2 -1
  97. package/src/server/auth-guard.ts +4 -3
  98. package/src/server/auth.ts +5 -10
  99. package/src/server/decode-session.ts +3 -2
  100. package/src/server/with-auth.ts +2 -1
@@ -3,6 +3,8 @@
3
3
  * This is a lightweight version for browser usage
4
4
  */
5
5
 
6
+ import { errForLog } from './log-safe';
7
+
6
8
  // Decode base64url
7
9
  function base64urlDecode(base64url: string): string {
8
10
  try {
@@ -39,7 +41,7 @@ export function jwtDecode<T = any>(token: string): T | null {
39
41
 
40
42
  return parsedPayload;
41
43
  } catch (e) {
42
- console.error('[JWT] Decode failed:', e instanceof Error ? e.message : 'Unknown error');
44
+ console.error('[JWT] Decode failed:', errForLog(e));
43
45
  return null;
44
46
  }
45
47
  }
@@ -1,4 +1,5 @@
1
1
  import { jwtDecode as originalJwtDecode } from 'jwt-decode';
2
+ import { errForLog } from './log-safe';
2
3
 
3
4
  // Define JwtPayload interface since jwt-decode v4 doesn't export it
4
5
  export interface JwtPayload {
@@ -65,7 +66,7 @@ export function decodeJwtHeader(token: string): JwtHeader | null {
65
66
 
66
67
  return JSON.parse(headerJson) as JwtHeader;
67
68
  } catch (error) {
68
- console.error('[JWT_DECODE] Failed to decode JWT header:', error);
69
+ console.error('[JWT_DECODE] Failed to decode JWT header:', errForLog(error));
69
70
  return null;
70
71
  }
71
72
  }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * log-safe: the ONLY way a caught error or an upstream `code` goes into a server log (PII-in-logs census, BAPert 76553 group 1; ported from the
3
+ * portal's api-error-response.ts, BAPert 76470).
4
+ *
5
+ * Server logs persist and are read beyond the person who made the request. An error's `message`, `stack`, `cause` and `details` can carry an
6
+ * email, a phone number, a code, a token, a SQL fragment or a host: an IdP error's message is the upstream's own free text, and V8's JSON parse
7
+ * error quotes its input. So a log line gets the error's NAME and a closed STATUS / CODE, nothing else. Every field is matched against a closed
8
+ * shape; anything that does not match is dropped, not truncated.
9
+ *
10
+ * No imports on purpose: this is a leaf, loaded by modules that must not pull in anything else.
11
+ */
12
+
13
+ /** An upstream `code` is loggable only if it is a closed token (UPPER_SNAKE). Anything else, including free text, becomes null. */
14
+ export function closedCodeForLog(code: unknown): string | null {
15
+ return typeof code === 'string' && /^[A-Z0-9_]{1,64}$/.test(code) ? code : null;
16
+ }
17
+
18
+ /**
19
+ * Describe a caught error for a log line: `error=<Name> [status=<n>] [code=<CLOSED_CODE>]`.
20
+ * It NEVER carries `message`, `stack`, `cause` or `details`. A name that is not a short identifier (a hostile or free-text `name`) becomes
21
+ * `NonError`; a status outside 100-599 is dropped; a code outside the closed token shape is dropped.
22
+ */
23
+ export function errForLog(err: unknown): string {
24
+ const e = (err !== null && typeof err === 'object' ? err : {}) as { name?: unknown; status?: unknown; code?: unknown };
25
+ const name = typeof e.name === 'string' && /^[A-Za-z0-9_]{1,48}$/.test(e.name) ? e.name : 'NonError';
26
+ const status = typeof e.status === 'number' && Number.isInteger(e.status) && e.status >= 100 && e.status <= 599 ? ` status=${e.status}` : '';
27
+ const closed = closedCodeForLog(e.code);
28
+ const code = closed ? ` code=${closed}` : '';
29
+ return `error=${name}${status}${code}`;
30
+ }
package/src/lib/redis.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  // E:\Repos\PayEz-Next-MVP\packages\next-mvp\src\lib\redis.ts
2
2
  import Redis, { RedisOptions } from 'ioredis';
3
+ import { errForLog } from './log-safe';
3
4
 
4
5
  let client: Redis | null = null;
5
6
 
@@ -76,7 +77,7 @@ function createClient(): Redis {
76
77
  // unhandled EventEmitter error, which can take the whole PROCESS down -
77
78
  // a Redis blip becoming a dead server is worse than the spinner was.
78
79
  instance.on('error', (err) => {
79
- console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${err instanceof Error ? err.message : String(err)}`);
80
+ console.error(`[redis] PATH FAILURE ${new Date().toISOString()}: ${errForLog(err)}`);
80
81
  });
81
82
 
82
83
  // 185121 line 3: the 16:05 event left no named trigger because ONLY 'error'
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { knownPasswordRules } from './password-rules';
18
+ import { errForLog } from './log-safe';
18
19
 
19
20
  export interface IdpErrorProjection {
20
21
  code?: string;
@@ -25,10 +26,14 @@ export interface IdpErrorProjection {
25
26
 
26
27
  const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
27
28
 
28
- /** Log an exception with a fresh correlationId and return the id for the response body. */
29
+ /**
30
+ * Log an exception with a fresh correlationId and return the id for the response body.
31
+ * The log line carries the error's NAME and closed code/status only (errForLog), never its message, stack or cause: those can hold an
32
+ * email, a phone number, a token or a quoted request body (PII-in-logs, BAPert 76553 group 1). The correlationId still ties the response to the line.
33
+ */
29
34
  export function logExceptionWithCorrelation(tag: string, err: unknown): string {
30
35
  const correlationId = globalThis.crypto.randomUUID();
31
- console.error(tag, { correlationId, error: err });
36
+ console.error(tag, { correlationId, error: errForLog(err) });
32
37
  return correlationId;
33
38
  }
34
39
 
@@ -15,6 +15,7 @@ import { getSessionPrefix, getRefreshLockPrefix, getAppSlug } from './app-slug';
15
15
  import { extractKidFromToken, decodeIdpAccessToken } from '../auth/utils/token-utils';
16
16
  import { tokenLogId } from './token-log-id';
17
17
  import { refreshTokenIdentity } from './refresh-in-doubt';
18
+ import { errForLog } from './log-safe';
18
19
 
19
20
  // Re-export SessionData for consumers
20
21
  export type { SessionData } from '../models/SessionModel';
@@ -65,7 +66,7 @@ export async function createSession(data: SessionData): Promise<string> {
65
66
  .setex(versionKey, SESSION_TTL, '1')
66
67
  .exec();
67
68
  } catch (error) {
68
- console.error('[SESSION-STORE] Failed to create session:', error);
69
+ console.error('[SESSION-STORE] Failed to create session:', errForLog(error));
69
70
  throw error;
70
71
  }
71
72
 
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { getRedis } from './redis';
14
+ import { closedCodeForLog, errForLog } from './log-safe';
14
15
 
15
16
  // Redis key for site logs (separate from data_logs)
16
17
  const REDIS_SITE_LOG_KEY = 'vibe:site-logs:pending';
@@ -74,11 +75,11 @@ export async function logSiteEvent(entry: SiteLogEntry): Promise<void> {
74
75
  console.log('[site-logger] Redis push success, queue length:', result);
75
76
  redis.expire(REDIS_SITE_LOG_KEY, REDIS_LOG_TTL).catch(() => {});
76
77
  }).catch(err => {
77
- console.error('[site-logger] Redis push failed:', err.message);
78
+ console.error('[site-logger] Redis push failed:', errForLog(err));
78
79
  });
79
80
  } catch (error) {
80
81
  // Fail silently - logging should never break the app
81
- console.error('[site-logger] Failed to log event:', error);
82
+ console.error('[site-logger] Failed to log event:', errForLog(error));
82
83
  }
83
84
  }
84
85
 
@@ -120,12 +121,14 @@ export const siteEvents = {
120
121
  });
121
122
  },
122
123
 
123
- loginFailed: (opts: { reason: string; email?: string; url?: string; user_agent?: string; ip_address?: string }) => {
124
+ // BREAKING (PII-in-logs group 3b, BAPert 76553 Q2): no `email` parameter. A failed login is told apart by a CLOSED reason code (UPPER_SNAKE),
125
+ // never by the address that was tried; anything that is not a closed code is stored as UNSPECIFIED. A caller that still passes `email` fails to compile.
126
+ loginFailed: (opts: { reason: string; url?: string; user_agent?: string; ip_address?: string }) => {
124
127
  logSiteEvent({
125
128
  level: 'warn',
126
129
  category: 'auth',
127
130
  message: 'Login failed',
128
- context: { reason: opts.reason, email: opts.email },
131
+ context: { reason: closedCodeForLog(opts.reason) ?? 'UNSPECIFIED' },
129
132
  url: opts.url || '/auth/login',
130
133
  user_agent: opts.user_agent,
131
134
  ip_address: opts.ip_address,
@@ -214,12 +217,15 @@ export const siteEvents = {
214
217
  });
215
218
  },
216
219
 
217
- error: (opts: { message: string; error?: string; stack?: string; user_id?: string | number; url?: string; user_agent?: string; ip_address?: string }) => {
220
+ // BREAKING (PII-in-logs group 3b, BAPert 76553 Q2): takes a CLOSED code, not caller text, an error string or a stack. The stored message is the fixed
221
+ // sentence 'Application error'; the code (UPPER_SNAKE, else UNSPECIFIED) goes in context. A caller that still passes `message`, `error` or `stack`
222
+ // fails to compile.
223
+ error: (opts: { code: string; user_id?: string | number; url?: string; user_agent?: string; ip_address?: string }) => {
218
224
  logSiteEvent({
219
225
  level: 'error',
220
226
  category: 'error',
221
- message: opts.message,
222
- context: { error: opts.error, stack: opts.stack },
227
+ message: 'Application error',
228
+ context: { code: closedCodeForLog(opts.code) ?? 'UNSPECIFIED' },
223
229
  user_id: opts.user_id,
224
230
  url: opts.url,
225
231
  user_agent: opts.user_agent,
@@ -9,6 +9,7 @@
9
9
  // ========================================================================================
10
10
 
11
11
  import { authClient } from '../client/better-auth-client';
12
+ import { errForLog } from './log-safe';
12
13
 
13
14
  /** Unwrap Better Auth getSession() to a flat session-like object for backward compat */
14
15
  async function getSession(): Promise<any> {
@@ -147,7 +148,7 @@ function scheduleLoginRedirect(isImmediate = false) {
147
148
  console.warn(`🔄 Redirecting to login with return URL: ${returnUrl}`);
148
149
  window.location.href = `/account-auth/login?returnUrl=${returnUrl}`;
149
150
  } catch (error) {
150
- console.error('❌ Error during login redirect:', error);
151
+ console.error('❌ Error during login redirect:', errForLog(error));
151
152
  // Final fallback
152
153
  try {
153
154
  window.location.href = '/account-auth/login';
@@ -721,7 +722,7 @@ class StandardizedClientApiService {
721
722
  }
722
723
 
723
724
  // Wrap unknown errors as network errors
724
- console.error('❌ API request failed:', error);
725
+ console.error('❌ API request failed:', errForLog(error));
725
726
  throw new ApiNetworkError(
726
727
  error instanceof Error ? error.message : 'Network error',
727
728
  0,
@@ -27,6 +27,7 @@ import { getRedis } from './redis';
27
27
  import { getAppSlug } from './app-slug';
28
28
  import { tokenLogId } from './token-log-id';
29
29
  import { REFRESH_WINDOW_MS } from './refresh-window';
30
+ import { errForLog } from './log-safe';
30
31
 
31
32
  // "needs refresh" threshold: THE one freshness window (lib/refresh-window), the same value the refresh route, ensureFreshAccessToken and idp-fetch use.
32
33
  const REFRESH_THRESHOLD_MS = REFRESH_WINDOW_MS;
@@ -434,7 +435,7 @@ export async function ensureFreshToken(
434
435
  }
435
436
  } catch (e) {
436
437
  // If we can't decode, proceed with normal logic
437
- console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', e);
438
+ console.warn('[TOKEN_LIFECYCLE] Could not validate JWT exp claim:', errForLog(e));
438
439
  }
439
440
  }
440
441
 
@@ -512,7 +513,7 @@ export async function ensureFreshToken(
512
513
  sessionData,
513
514
  };
514
515
  } catch (error) {
515
- console.error('[TOKEN_LIFECYCLE] Error:', error);
516
+ console.error('[TOKEN_LIFECYCLE] Error:', errForLog(error));
516
517
  return {
517
518
  success: false,
518
519
  error: 'NO_SESSION',
@@ -32,6 +32,7 @@ import {
32
32
  resetSecret,
33
33
  signedResetHref,
34
34
  } from '../auth/session-reset-link';
35
+ import { errForLog } from '../lib/log-safe';
35
36
 
36
37
  // =============================================================================
37
38
  // TYPES
@@ -1037,7 +1038,7 @@ async function handleAllow(
1037
1038
  );
1038
1039
  }
1039
1040
  } catch (error) {
1040
- console.error('[MIDDLEWARE] RBAC error:', error);
1041
+ console.error('[MIDDLEWARE] RBAC error:', errForLog(error));
1041
1042
 
1042
1043
  // In development, fail open
1043
1044
  if (process.env.NODE_ENV !== 'production') {
@@ -1,3 +1,4 @@
1
+ import { errForLog } from '../lib/log-safe';
1
2
  /**
2
3
  * Page RBAC Check Module
3
4
  *
@@ -262,7 +263,7 @@ export async function checkPagePermission(
262
263
  };
263
264
  }
264
265
 
265
- console.error('[RBAC] Proxy error:', error);
266
+ console.error('[RBAC] Proxy error:', errForLog(error));
266
267
  return {
267
268
  allowed: false,
268
269
  reason: 'rbac_service_unavailable',
@@ -27,6 +27,7 @@ import {
27
27
  } from '../../lib/app-slug';
28
28
  import { siteEvents, getClientIp } from '../../lib/site-logger';
29
29
  import { logExceptionWithCorrelation } from '../../lib/safe-error-response';
30
+ import { errForLog } from '../../lib/log-safe';
30
31
 
31
32
  /**
32
33
  * 4.8.9: answered (status 500) when a sign-out did not finish. 4.8.10 (BAPert 76297): TWO sentences, one per cause, each true for its
@@ -79,7 +80,13 @@ export async function POST(req: NextRequest) {
79
80
  await revokeSessionAtIdp(req);
80
81
  } catch (error) {
81
82
  idpRevokeFailed = true;
82
- console.error('[LOGOUT_ROUTE] IdP revoke failed - the IdP refresh token is STILL LIVE:', error);
83
+ // "STILL LIVE" only when the IdP showed it (a forced refresh succeeded); otherwise say what is known: it was not confirmed.
84
+ console.error(
85
+ (error as { grantLive?: boolean } | null)?.grantLive === true
86
+ ? '[LOGOUT_ROUTE] IdP revoke failed - the IdP refresh token is STILL LIVE:'
87
+ : '[LOGOUT_ROUTE] IdP revoke could not be confirmed (the grant may already be dead):',
88
+ errForLog(error),
89
+ );
83
90
  }
84
91
  }
85
92
 
@@ -91,7 +98,7 @@ export async function POST(req: NextRequest) {
91
98
  console.info('[LOGOUT_ROUTE] Session deleted from store');
92
99
  } catch (error) {
93
100
  localDeleteFailed = true;
94
- console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', error);
101
+ console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', errForLog(error));
95
102
  }
96
103
  }
97
104
 
@@ -30,6 +30,7 @@
30
30
  import { NextResponse } from 'next/server';
31
31
  import { getIDPClientConfig } from '../../lib/idp-client-config';
32
32
  import { secretSafeJson } from '../../lib/never-in-response';
33
+ import { errForLog } from '../../lib/log-safe';
33
34
 
34
35
  export interface PublicAuthSettings {
35
36
  // OAuth providers enabled for this client
@@ -95,7 +96,7 @@ export async function GET() {
95
96
  config = await getIDPClientConfig();
96
97
  } catch (error) {
97
98
  // Keep the loud line - it is what finally unmasked this in production.
98
- console.error('[AUTH_SETTINGS] Failed to get settings:', error);
99
+ console.error('[AUTH_SETTINGS] Failed to get settings:', errForLog(error));
99
100
  // The `degraded: true` shape that used to live here (240447) is gone
100
101
  // deliberately. It still answered `success: true` with a full invented
101
102
  // policy and merely LABELLED the guess - which BAPert v32 rules out:
@@ -14,6 +14,7 @@ import { decodeSession } from './decode-session';
14
14
  import { getIDPClientConfig } from '../lib/idp-client-config';
15
15
  import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
16
16
  import type { SessionData } from '../lib/session-store';
17
+ import { errForLog } from '../lib/log-safe';
17
18
 
18
19
  // =============================================================================
19
20
  // TYPES
@@ -89,7 +90,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
89
90
  decoded = await decodeSession();
90
91
  } catch (error) {
91
92
  // Redis unreachable or startup failure → fail closed
92
- console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
93
+ console.error('[AUTH-GUARD] Session decode failed (service error):', errForLog(error));
93
94
  redirect(serviceUnavailableUrl);
94
95
  }
95
96
 
@@ -122,7 +123,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
122
123
  requires2FA = config.authSettings?.require2FA ?? true;
123
124
  } catch (error) {
124
125
  // If we can't check 2FA config, fail closed
125
- console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
126
+ console.error('[AUTH-GUARD] 2FA config check failed:', errForLog(error));
126
127
  redirect(serviceUnavailableUrl);
127
128
  }
128
129
 
@@ -186,7 +187,7 @@ export async function authGuard(options?: AuthGuardOptions): Promise<AuthGuardRe
186
187
  if (error && typeof error === 'object' && 'digest' in error) {
187
188
  throw error;
188
189
  }
189
- console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
190
+ console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, errForLog(error));
190
191
  redirect(serviceUnavailableUrl);
191
192
  }
192
193
  }
@@ -14,6 +14,7 @@ import {
14
14
  type SessionData,
15
15
  } from '../lib/session-store';
16
16
  import { tokenLogId } from '../lib/token-log-id';
17
+ import { errForLog } from '../lib/log-safe';
17
18
  import { resolveVerifiedSessionTokenFromRequest } from './verified-session-token';
18
19
 
19
20
  export type IdpTokenResult =
@@ -134,20 +135,14 @@ export async function getAuthInstance() {
134
135
  * `{error:'NO_SESSION'}`, and getSession() swallowed every Redis error into a bare `catch {}`. These helpers name
135
136
  * the origin and make a swallowed read audible. Hard rules: every helper is wrapped so it can NEVER throw or change
136
137
  * a return value; nothing here ever logs a token, a cookie VALUE or a Redis value - only a tokenLogId() fingerprint,
137
- * cookie NAMES, booleans, and an error name/message with any long opaque run redacted.
138
+ * cookie NAMES, booleans, and an error NAME with its closed code/status (errForLog; never the message).
138
139
  * -------------------------------------------------------------------------------------------------------------- */
139
140
 
140
- /** An error name/message, first 80 chars, with any 24+ char opaque run (a token, a key) replaced. */
141
- function scrubForLog(v: unknown): string {
142
- const raw = v instanceof Error ? v.message : String(v ?? '');
143
- return raw.replace(/[A-Za-z0-9_\-.+/=]{24,}/g, '[redacted]').slice(0, 80);
144
- }
145
-
146
- /** `catch {}` sites on the session read path: still swallowed, now audible. */
141
+ /** `catch {}` sites on the session read path: still swallowed, now audible. The line names the error and its closed code/status only: the
142
+ * message is dropped (BAPert 76553: a redacted-and-truncated message still leaked host names, ports and quoted input). */
147
143
  function logSessionReadSwallowed(path: 'primary' | 'A' | 'B', err: unknown): void {
148
144
  try {
149
- const name = scrubForLog(err instanceof Error ? err.name : typeof err);
150
- console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} err=${name} msg=${scrubForLog(err)}`);
145
+ console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} ${errForLog(err)}`);
151
146
  } catch {
152
147
  /* a diagnostic must never change the outcome */
153
148
  }
@@ -15,6 +15,7 @@ import { ensureInitialized } from '../lib/startup-init';
15
15
  import { extractSessionTokenFromCookieValue } from '../lib/session-cookie-shape';
16
16
  import { resolveVerifiedSessionCookie } from './verified-session-token';
17
17
  import { tokenLogId } from '../lib/token-log-id';
18
+ import { errForLog } from '../lib/log-safe';
18
19
 
19
20
  export interface DecodedSession {
20
21
  sessionData: SessionData;
@@ -197,7 +198,7 @@ async function tryBetterAuthSession(
197
198
 
198
199
  return { sessionData, jwtPayload };
199
200
  } catch (error) {
200
- console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
201
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', errForLog(error));
201
202
  return null;
202
203
  }
203
204
  }
@@ -303,7 +304,7 @@ export async function decodeSession(
303
304
  jwtPayload: (payload ?? { sessionToken }) as DecodedSession['jwtPayload'],
304
305
  };
305
306
  } catch (error) {
306
- console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
307
+ console.error('[DECODE-SESSION] Error:', errForLog(error));
307
308
  return null;
308
309
  }
309
310
  }
@@ -17,6 +17,7 @@ import 'server-only';
17
17
  import { NextRequest, NextResponse } from 'next/server';
18
18
  import { decodeSession } from './decode-session';
19
19
  import type { SessionData } from '../lib/session-store';
20
+ import { errForLog } from '../lib/log-safe';
20
21
 
21
22
  // =============================================================================
22
23
  // TYPES
@@ -83,7 +84,7 @@ export function withAuth(
83
84
 
84
85
  return handler(req, auth);
85
86
  } catch (error) {
86
- console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
87
+ console.error('[WITH-AUTH] Error:', errForLog(error));
87
88
  return NextResponse.json(
88
89
  { error: 'Internal Server Error', message: 'Auth check failed' },
89
90
  { status: 500 }