@payez/next-mvp 4.11.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +13 -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 +2 -1
  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 +2 -1
  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
@@ -31,6 +31,7 @@ const app_slug_1 = require("../lib/app-slug");
31
31
  const rbac_check_1 = require("./rbac-check");
32
32
  const session_cookie_clear_1 = require("../auth/session-cookie-clear");
33
33
  const session_reset_link_1 = require("../auth/session-reset-link");
34
+ const log_safe_1 = require("../lib/log-safe");
34
35
  // =============================================================================
35
36
  // DEFAULT IMPLEMENTATIONS
36
37
  // =============================================================================
@@ -748,7 +749,7 @@ async function handleAllow(request, pathname, sessionPointer, sessionStatus, rba
748
749
  }
749
750
  }
750
751
  catch (error) {
751
- console.error('[MIDDLEWARE] RBAC error:', error);
752
+ console.error('[MIDDLEWARE] RBAC error:', (0, log_safe_1.errForLog)(error));
752
753
  // In development, fail open
753
754
  if (process.env.NODE_ENV !== 'production') {
754
755
  console.warn('[MIDDLEWARE] RBAC: Allowing in development despite error');
@@ -1,16 +1,3 @@
1
- /**
2
- * Page RBAC Check Module
3
- *
4
- * Checks page-level permissions via Vibe API through the IDP Proxy.
5
- * Uses in-memory cache to reduce API calls.
6
- * Fails closed (DENY) on errors or timeout.
7
- *
8
- * All requests route through the IDP Vibe Proxy ({IDP_URL}/api/vibe/proxy)
9
- * which injects proper HMAC credentials for the Vibe API.
10
- *
11
- * @version 2.0.0
12
- * @since page-rbac-2026-01
13
- */
14
1
  export interface RBACResult {
15
2
  allowed: boolean;
16
3
  requires_2fa?: boolean;
@@ -1,4 +1,9 @@
1
1
  "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.clearRBACCache = clearRBACCache;
4
+ exports.checkPagePermission = checkPagePermission;
5
+ exports.isRBACEnabled = isRBACEnabled;
6
+ const log_safe_1 = require("../lib/log-safe");
2
7
  /**
3
8
  * Page RBAC Check Module
4
9
  *
@@ -12,10 +17,6 @@
12
17
  * @version 2.0.0
13
18
  * @since page-rbac-2026-01
14
19
  */
15
- Object.defineProperty(exports, "__esModule", { value: true });
16
- exports.clearRBACCache = clearRBACCache;
17
- exports.checkPagePermission = checkPagePermission;
18
- exports.isRBACEnabled = isRBACEnabled;
19
20
  // ============================================================================
20
21
  // WEB CRYPTO HELPERS (Edge Runtime compatible)
21
22
  // ============================================================================
@@ -203,7 +204,7 @@ async function checkPagePermission(path, userRoles, clientId, userClaims) {
203
204
  redirect: '/error?code=rbac_timeout',
204
205
  };
205
206
  }
206
- console.error('[RBAC] Proxy error:', error);
207
+ console.error('[RBAC] Proxy error:', (0, log_safe_1.errForLog)(error));
207
208
  return {
208
209
  allowed: false,
209
210
  reason: 'rbac_service_unavailable',
@@ -24,6 +24,7 @@ const idp_logout_1 = require("../../lib/idp-logout");
24
24
  const app_slug_1 = require("../../lib/app-slug");
25
25
  const site_logger_1 = require("../../lib/site-logger");
26
26
  const safe_error_response_1 = require("../../lib/safe-error-response");
27
+ const log_safe_1 = require("../../lib/log-safe");
27
28
  /**
28
29
  * 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
29
30
  * cause, neither promising a retry (the local session is gone, so there is nothing to retry from). The browser's cookies and the local
@@ -66,7 +67,10 @@ async function POST(req) {
66
67
  }
67
68
  catch (error) {
68
69
  idpRevokeFailed = true;
69
- console.error('[LOGOUT_ROUTE] IdP revoke failed - the IdP refresh token is STILL LIVE:', error);
70
+ // "STILL LIVE" only when the IdP showed it (a forced refresh succeeded); otherwise say what is known: it was not confirmed.
71
+ console.error(error?.grantLive === true
72
+ ? '[LOGOUT_ROUTE] IdP revoke failed - the IdP refresh token is STILL LIVE:'
73
+ : '[LOGOUT_ROUTE] IdP revoke could not be confirmed (the grant may already be dead):', (0, log_safe_1.errForLog)(error));
70
74
  }
71
75
  }
72
76
  // Delete session from EVERY store — canonical + BA secondary (181922).
@@ -78,7 +82,7 @@ async function POST(req) {
78
82
  }
79
83
  catch (error) {
80
84
  localDeleteFailed = true;
81
- console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', error);
85
+ console.error('[LOGOUT_ROUTE] Failed to delete session - the server session is STILL LIVE:', (0, log_safe_1.errForLog)(error));
82
86
  }
83
87
  }
84
88
  // Log logout event (fire-and-forget)
@@ -32,6 +32,7 @@ exports.GET = GET;
32
32
  const server_1 = require("next/server");
33
33
  const idp_client_config_1 = require("../../lib/idp-client-config");
34
34
  const never_in_response_1 = require("../../lib/never-in-response");
35
+ const log_safe_1 = require("../../lib/log-safe");
35
36
  /**
36
37
  * Fail closed and LOUD. 503 + success:false + a code, and CRUCIALLY no policy
37
38
  * fields at all - not defaults, not partials. A consumer cannot mistake an
@@ -53,7 +54,7 @@ async function GET() {
53
54
  }
54
55
  catch (error) {
55
56
  // Keep the loud line - it is what finally unmasked this in production.
56
- console.error('[AUTH_SETTINGS] Failed to get settings:', error);
57
+ console.error('[AUTH_SETTINGS] Failed to get settings:', (0, log_safe_1.errForLog)(error));
57
58
  // The `degraded: true` shape that used to live here (240447) is gone
58
59
  // deliberately. It still answered `success: true` with a full invented
59
60
  // policy and merely LABELLED the guess - which BAPert v32 rules out:
@@ -14,6 +14,7 @@ const navigation_1 = require("next/navigation");
14
14
  const headers_1 = require("next/headers");
15
15
  const decode_session_1 = require("./decode-session");
16
16
  const idp_client_config_1 = require("../lib/idp-client-config");
17
+ const log_safe_1 = require("../lib/log-safe");
17
18
  // =============================================================================
18
19
  // CONSTANTS
19
20
  // =============================================================================
@@ -53,7 +54,7 @@ async function authGuard(options) {
53
54
  }
54
55
  catch (error) {
55
56
  // Redis unreachable or startup failure → fail closed
56
- console.error('[AUTH-GUARD] Session decode failed (service error):', error instanceof Error ? error.message : String(error));
57
+ console.error('[AUTH-GUARD] Session decode failed (service error):', (0, log_safe_1.errForLog)(error));
57
58
  (0, navigation_1.redirect)(serviceUnavailableUrl);
58
59
  }
59
60
  // No session at all → redirect to login
@@ -83,7 +84,7 @@ async function authGuard(options) {
83
84
  }
84
85
  catch (error) {
85
86
  // If we can't check 2FA config, fail closed
86
- console.error('[AUTH-GUARD] 2FA config check failed:', error instanceof Error ? error.message : String(error));
87
+ console.error('[AUTH-GUARD] 2FA config check failed:', (0, log_safe_1.errForLog)(error));
87
88
  (0, navigation_1.redirect)(serviceUnavailableUrl);
88
89
  }
89
90
  if (requires2FA) {
@@ -144,7 +145,7 @@ async function authGuard(options) {
144
145
  if (error && typeof error === 'object' && 'digest' in error) {
145
146
  throw error;
146
147
  }
147
- console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, error instanceof Error ? error.message : String(error));
148
+ console.error(`[AUTH-GUARD] Custom check "${check.name}" error:`, (0, log_safe_1.errForLog)(error));
148
149
  (0, navigation_1.redirect)(serviceUnavailableUrl);
149
150
  }
150
151
  }
@@ -50,6 +50,7 @@ require("server-only");
50
50
  const better_auth_1 = require("../auth/better-auth");
51
51
  const session_store_1 = require("../lib/session-store");
52
52
  const token_log_id_1 = require("../lib/token-log-id");
53
+ const log_safe_1 = require("../lib/log-safe");
53
54
  const verified_session_token_1 = require("./verified-session-token");
54
55
  function buildSessionDataFromAuthSession(session) {
55
56
  const user = session?.user;
@@ -147,18 +148,13 @@ async function getAuthInstance() {
147
148
  * `{error:'NO_SESSION'}`, and getSession() swallowed every Redis error into a bare `catch {}`. These helpers name
148
149
  * the origin and make a swallowed read audible. Hard rules: every helper is wrapped so it can NEVER throw or change
149
150
  * a return value; nothing here ever logs a token, a cookie VALUE or a Redis value - only a tokenLogId() fingerprint,
150
- * cookie NAMES, booleans, and an error name/message with any long opaque run redacted.
151
+ * cookie NAMES, booleans, and an error NAME with its closed code/status (errForLog; never the message).
151
152
  * -------------------------------------------------------------------------------------------------------------- */
152
- /** An error name/message, first 80 chars, with any 24+ char opaque run (a token, a key) replaced. */
153
- function scrubForLog(v) {
154
- const raw = v instanceof Error ? v.message : String(v ?? '');
155
- return raw.replace(/[A-Za-z0-9_\-.+/=]{24,}/g, '[redacted]').slice(0, 80);
156
- }
157
- /** `catch {}` sites on the session read path: still swallowed, now audible. */
153
+ /** `catch {}` sites on the session read path: still swallowed, now audible. The line names the error and its closed code/status only: the
154
+ * message is dropped (BAPert 76553: a redacted-and-truncated message still leaked host names, ports and quoted input). */
158
155
  function logSessionReadSwallowed(path, err) {
159
156
  try {
160
- const name = scrubForLog(err instanceof Error ? err.name : typeof err);
161
- console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} err=${name} msg=${scrubForLog(err)}`);
157
+ console.warn(`[server-auth] SESSION_READ_SWALLOWED path=${path} ${(0, log_safe_1.errForLog)(err)}`);
162
158
  }
163
159
  catch {
164
160
  /* a diagnostic must never change the outcome */
@@ -49,6 +49,7 @@ const startup_init_1 = require("../lib/startup-init");
49
49
  const session_cookie_shape_1 = require("../lib/session-cookie-shape");
50
50
  const verified_session_token_1 = require("./verified-session-token");
51
51
  const token_log_id_1 = require("../lib/token-log-id");
52
+ const log_safe_1 = require("../lib/log-safe");
52
53
  /**
53
54
  * Try Better Auth's server-side session API.
54
55
  * Returns a DecodedSession if Better Auth has an active session, null otherwise.
@@ -215,7 +216,7 @@ async function tryBetterAuthSession(requestCookies) {
215
216
  return { sessionData, jwtPayload };
216
217
  }
217
218
  catch (error) {
218
- console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
219
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', (0, log_safe_1.errForLog)(error));
219
220
  return null;
220
221
  }
221
222
  }
@@ -304,7 +305,7 @@ async function decodeSession(requestCookies) {
304
305
  };
305
306
  }
306
307
  catch (error) {
307
- console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
308
+ console.error('[DECODE-SESSION] Error:', (0, log_safe_1.errForLog)(error));
308
309
  return null;
309
310
  }
310
311
  }
@@ -18,6 +18,7 @@ exports.withAuth = withAuth;
18
18
  require("server-only");
19
19
  const server_1 = require("next/server");
20
20
  const decode_session_1 = require("./decode-session");
21
+ const log_safe_1 = require("../lib/log-safe");
21
22
  // =============================================================================
22
23
  // MAIN
23
24
  // =============================================================================
@@ -52,7 +53,7 @@ function withAuth(handler, options) {
52
53
  return handler(req, auth);
53
54
  }
54
55
  catch (error) {
55
- console.error('[WITH-AUTH] Error:', error instanceof Error ? error.message : String(error));
56
+ console.error('[WITH-AUTH] Error:', (0, log_safe_1.errForLog)(error));
56
57
  return server_1.NextResponse.json({ error: 'Internal Server Error', message: 'Auth check failed' }, { status: 500 });
57
58
  }
58
59
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@payez/next-mvp",
3
- "version": "4.11.0",
3
+ "version": "4.12.0",
4
4
  "sideEffects": false,
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -3,6 +3,7 @@ import { ENV_CONFIG } from '../../../config/env';
3
3
  import { getIDPClientConfig } from '../../../lib/idp-client-config';
4
4
  import { resolveClientKey } from '../../../lib/client-key';
5
5
  import { idpErrorEnvelope } from '../../../lib/safe-error-response';
6
+ import { errForLog } from '../../../lib/log-safe';
6
7
 
7
8
  /**
8
9
  * Account recovery - the ONE forwarder behind /recovery/send-code and /recovery/verify-code (PAY-1588).
@@ -57,7 +58,7 @@ async function recoveryClientKey(where: string): Promise<string | null> {
57
58
  const config = await getIDPClientConfig();
58
59
  return resolveClientKey(config, where).key;
59
60
  } catch (e) {
60
- console.error(`[${where}] could not resolve IDP client config for client_id:`, e);
61
+ console.error(`[${where}] could not resolve IDP client config for client_id:`, errForLog(e));
61
62
  return null;
62
63
  }
63
64
  }
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getIDPClientConfig } from '../../../lib/idp-client-config';
3
3
  import { resolveClientKey } from '../../../lib/client-key';
4
4
  import { idpErrorEnvelope } from '../../../lib/safe-error-response';
5
+ import { errForLog } from '../../../lib/log-safe';
5
6
 
6
7
  function getIdpUrl(): string {
7
8
  const url = process.env.IDP_URL;
@@ -34,7 +35,7 @@ async function recoveryClientKey(): Promise<string | null> {
34
35
  const config = await getIDPClientConfig();
35
36
  return resolveClientKey(config, 'account/recovery/initiate').key;
36
37
  } catch (e) {
37
- console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', e);
38
+ console.error('[account/recovery/initiate] could not resolve IDP client config for client_id:', errForLog(e));
38
39
  return null;
39
40
  }
40
41
  }
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getIDPClientConfig } from '../../lib/idp-client-config';
3
3
  import { resolveClientKey } from '../../lib/client-key';
4
4
  import { idpErrorEnvelope, projectPasswordPolicyRules, projectResetTokenInvalid, projectPasswordBreached } from '../../lib/safe-error-response';
5
+ import { errForLog } from '../../lib/log-safe';
5
6
 
6
7
  function getIdpUrl(): string {
7
8
  const url = process.env.IDP_URL;
@@ -34,7 +35,7 @@ async function recoveryClientKey(): Promise<string | null> {
34
35
  const config = await getIDPClientConfig();
35
36
  return resolveClientKey(config, 'account/reset-password').key;
36
37
  } catch (e) {
37
- console.error('[account/reset-password] could not resolve IDP client config for client_id:', e);
38
+ console.error('[account/reset-password] could not resolve IDP client config for client_id:', errForLog(e));
38
39
  return null;
39
40
  }
40
41
  }
@@ -11,6 +11,7 @@ import { NextRequest, NextResponse } from 'next/server';
11
11
  import { idpFetchJSON } from '../../lib/idp-fetch';
12
12
  import { ENV_CONFIG } from '../../config/env';
13
13
  import { twoFactorSendCodeFailure } from '../../lib/two-factor-verify-failure';
14
+ import { errForLog } from '../../lib/log-safe';
14
15
 
15
16
  export async function POST(req: NextRequest) {
16
17
  try {
@@ -57,7 +58,7 @@ export async function POST(req: NextRequest) {
57
58
  { status: 200 }
58
59
  );
59
60
  } catch (error) {
60
- console.error('[SEND_CODE] Error:', error);
61
+ console.error('[SEND_CODE] Error:', errForLog(error));
61
62
  return NextResponse.json(
62
63
  {
63
64
  success: false,
@@ -1,6 +1,7 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { idpFetchJSON } from '../../lib/idp-fetch';
3
3
  import { ENV_CONFIG } from '../../config/env';
4
+ import { errForLog } from '../../lib/log-safe';
4
5
 
5
6
  /**
6
7
  * Update Phone Number API Handler
@@ -75,7 +76,7 @@ export async function POST(req: NextRequest) {
75
76
  data: responseData,
76
77
  });
77
78
  } catch (error) {
78
- console.error('[UPDATE_PHONE] Error:', error);
79
+ console.error('[UPDATE_PHONE] Error:', errForLog(error));
79
80
  return NextResponse.json(
80
81
  { success: false, error: { code: 'INTERNAL_ERROR', message: 'Failed to update phone number' } },
81
82
  { status: 500 }
@@ -1,4 +1,5 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
+ import { errForLog } from '../../lib/log-safe';
2
3
 
3
4
  interface ValidatePasswordRequest {
4
5
  password: string;
@@ -123,7 +124,7 @@ export async function POST(req: NextRequest) {
123
124
  headers: { 'Cache-Control': 'no-store' },
124
125
  });
125
126
  } catch (error) {
126
- console.error('[VALIDATE_PASSWORD] Error:', error);
127
+ console.error('[VALIDATE_PASSWORD] Error:', errForLog(error));
127
128
  return NextResponse.json(
128
129
  {
129
130
  is_valid: false,
@@ -14,6 +14,7 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
14
14
  import { transitionTo2FASession } from '../../lib/session-store';
15
15
  import { jwtDecode } from '../../lib/jwt-decode';
16
16
  import { twoFactorVerifyFailure } from '../../lib/two-factor-verify-failure';
17
+ import { errForLog } from '../../lib/log-safe';
17
18
 
18
19
  export async function POST(req: NextRequest) {
19
20
  try {
@@ -121,7 +122,7 @@ export async function POST(req: NextRequest) {
121
122
  message: unwrappedData.message || 'Email code verified successfully'
122
123
  }, { status: 200 });
123
124
  } catch (error) {
124
- console.error('[VERIFY_EMAIL] Error:', error);
125
+ console.error('[VERIFY_EMAIL] Error:', errForLog(error));
125
126
  return NextResponse.json(
126
127
  {
127
128
  success: false,
@@ -14,6 +14,7 @@ import { getTokenTestAware } from '../../lib/test-aware-get-token';
14
14
  import { transitionTo2FASession } from '../../lib/session-store';
15
15
  import { jwtDecode } from '../../lib/jwt-decode';
16
16
  import { twoFactorVerifyFailure } from '../../lib/two-factor-verify-failure';
17
+ import { errForLog } from '../../lib/log-safe';
17
18
 
18
19
  export async function POST(req: NextRequest) {
19
20
  try {
@@ -121,7 +122,7 @@ export async function POST(req: NextRequest) {
121
122
  message: unwrappedData.message || 'SMS code verified successfully'
122
123
  }, { status: 200 });
123
124
  } catch (error) {
124
- console.error('[VERIFY_SMS] Error:', error);
125
+ console.error('[VERIFY_SMS] Error:', errForLog(error));
125
126
  return NextResponse.json(
126
127
  {
127
128
  success: false,
@@ -15,6 +15,7 @@ import {
15
15
  ANON_COOKIE_NAME,
16
16
  AnonSessionPreferences,
17
17
  } from '../../lib/anon-session';
18
+ import { errForLog } from '../../lib/log-safe';
18
19
 
19
20
  const COOKIE_MAX_AGE = 90 * 24 * 60 * 60; // 90 days in seconds
20
21
 
@@ -52,7 +53,7 @@ export async function GET(request: NextRequest) {
52
53
 
53
54
  return response;
54
55
  } catch (error) {
55
- console.error('[ANON-PREFERENCES] GET error:', error);
56
+ console.error('[ANON-PREFERENCES] GET error:', errForLog(error));
56
57
  return NextResponse.json(
57
58
  { success: false, error: 'Failed to get preferences' },
58
59
  { status: 500 }
@@ -114,7 +115,7 @@ export async function POST(request: NextRequest) {
114
115
 
115
116
  return response;
116
117
  } catch (error) {
117
- console.error('[ANON-PREFERENCES] POST error:', error);
118
+ console.error('[ANON-PREFERENCES] POST error:', errForLog(error));
118
119
  return NextResponse.json(
119
120
  { success: false, error: 'Failed to update preferences' },
120
121
  { status: 500 }
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from 'next/server';
2
2
  import { getIDPClientConfig } from '../../lib/idp-client-config';
3
3
  import { resolveClientKey } from '../../lib/client-key';
4
4
  import { bearerFetch } from '../../lib/bearer-fetch';
5
+ import { errForLog } from '../../lib/log-safe';
5
6
 
6
7
  // `export const runtime = 'nodejs'` must be written LITERALLY in the app's route file (Next ignores a re-exported segment config).
7
8
 
@@ -43,7 +44,7 @@ export async function POST(req: NextRequest) {
43
44
  const config = await getIDPClientConfig();
44
45
  clientKey = resolveClientKey(config, 'graft/admin-login/email-code').key ?? '';
45
46
  } catch (err) {
46
- console.error('[ADMIN_LOGIN_EMAIL_CODE] IDP client config unavailable:', err instanceof Error ? err.message : String(err));
47
+ console.error('[ADMIN_LOGIN_EMAIL_CODE] IDP client config unavailable:', errForLog(err));
47
48
  return NextResponse.json(
48
49
  { success: false, error: 'Sign-in is temporarily unavailable. This is a problem on our side. Please try again shortly.' },
49
50
  { status: 503 },
@@ -74,7 +75,7 @@ export async function POST(req: NextRequest) {
74
75
  /* not JSON - nothing to echo */
75
76
  }
76
77
  } catch (err) {
77
- console.error('[ADMIN_LOGIN_EMAIL_CODE] twofa/email/send fetch failed:', err instanceof Error ? err.message : String(err));
78
+ console.error('[ADMIN_LOGIN_EMAIL_CODE] twofa/email/send fetch failed:', errForLog(err));
78
79
  }
79
80
 
80
81
  if (!sendOk) {
@@ -47,6 +47,7 @@ import { createHash } from 'crypto';
47
47
  import { DEVICE_ID_HEADER, isValidDeviceId } from '../../lib/device-id';
48
48
  import { classifyTwofaRejection, type TwofaLaneResult } from '../../lib/twofa-rejection';
49
49
  import { bearerFetch } from '../../lib/bearer-fetch';
50
+ import { errForLog } from '../../lib/log-safe';
50
51
 
51
52
  // `export const runtime = 'nodejs'` must be written LITERALLY in the app's route file (Next ignores a re-exported segment config).
52
53
 
@@ -182,7 +183,7 @@ export async function POST(req: NextRequest) {
182
183
  try {
183
184
  config = await getIDPClientConfig();
184
185
  } catch (err) {
185
- console.error('[ADMIN_LOGIN] IDP client config unavailable — returning 503:', err);
186
+ console.error('[ADMIN_LOGIN] IDP client config unavailable — returning 503:', errForLog(err));
186
187
  return NextResponse.json(
187
188
  {
188
189
  success: false,
@@ -283,7 +284,7 @@ export async function POST(req: NextRequest) {
283
284
  laneResults.push({ lane, status: idpResponse.status, body: await idpResponse.clone().text().catch(() => ''), retryAfter: idpResponse.headers.get('retry-after') });
284
285
  }
285
286
  } catch (error) {
286
- console.error('[ADMIN-LOGIN] twofa verify fetch failed:', error instanceof Error ? error.message : String(error));
287
+ console.error('[ADMIN-LOGIN] twofa verify fetch failed:', errForLog(error));
287
288
  return NextResponse.json({ success: false, error: 'Unable to reach the authentication service' }, { status: 503 });
288
289
  }
289
290
  if (!idpResponse.ok) {
@@ -372,7 +373,7 @@ export async function POST(req: NextRequest) {
372
373
  body: JSON.stringify(idpPayload),
373
374
  });
374
375
  } catch (error) {
375
- console.error('[ADMIN-LOGIN] IDP fetch failed:', error instanceof Error ? error.message : String(error));
376
+ console.error('[ADMIN-LOGIN] IDP fetch failed:', errForLog(error));
376
377
  return NextResponse.json({ success: false, error: 'Unable to reach the authentication service' }, { status: 503 });
377
378
  }
378
379
 
@@ -474,7 +475,7 @@ export async function POST(req: NextRequest) {
474
475
  console.warn('[ADMIN-LOGIN] mfa/methods unreadable, defaulting to email lane', mResp.status);
475
476
  }
476
477
  } catch (err) {
477
- console.warn('[ADMIN-LOGIN] mfa/methods fetch failed, defaulting to email lane:', err instanceof Error ? err.message : String(err));
478
+ console.warn('[ADMIN-LOGIN] mfa/methods fetch failed, defaulting to email lane:', errForLog(err));
478
479
  }
479
480
  }
480
481
  // PAY-2314: advertise what this user can actually use, derived from the SAME usableFactors that chose the lane
@@ -549,7 +550,7 @@ export async function POST(req: NextRequest) {
549
550
  // not JSON - nothing to echo, and never a reason to fail the sign-in
550
551
  }
551
552
  } catch (err) {
552
- console.error(`[ADMIN-LOGIN] twofa/${sendLane}/send fetch failed:`, err instanceof Error ? err.message : String(err));
553
+ console.error(`[ADMIN-LOGIN] twofa/${sendLane}/send fetch failed:`, errForLog(err));
553
554
  }
554
555
  }
555
556
  // 267479: `method:'email'` WAS A HARDCODED LITERAL HERE, and it was a lie for
@@ -26,6 +26,7 @@ import { getSession as getBetterAuthSession } from '../../server/auth';
26
26
  import { ensureFreshAccessToken, type EnsureFreshResult } from '../../lib/ensure-fresh-access-token';
27
27
  import { UPSTREAM_TIMEOUT_CODE } from '../../lib/refresh-in-doubt';
28
28
  import { sanitizeTraceId } from '../../lib/safe-trace-id';
29
+ import { errForLog } from '../../lib/log-safe';
29
30
 
30
31
  interface RefreshConfig {
31
32
  idpBaseUrl: string;
@@ -145,7 +146,7 @@ export function createRefreshHandler(config: RefreshConfig) {
145
146
  hasRefreshToken: result.hasRefreshToken,
146
147
  });
147
148
  } catch (err: any) {
148
- console.error('[AUTH_REFRESH] Unexpected error', { error: err?.message || String(err) });
149
+ console.error('[AUTH_REFRESH] Unexpected error', { error: errForLog(err) });
149
150
  return NextResponse.json({
150
151
  error: 'Unexpected error during refresh',
151
152
  code: 'INTERNAL_SERVER_ERROR'
@@ -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
- // Delete the session from EVERY store (181922): canonical + BA secondary.
151
- // Canonical-only deletion let the ba: record resurrect the session.
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
- // Log error but don't fail the signout process
160
- console.error('[SIGNOUT] Redis session cleanup failed', { error: sessionError });
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 = NextResponse.json(responseData);
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,