@payez/next-mvp 4.4.4 → 4.4.6

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 (189) hide show
  1. package/README.md +10 -3
  2. package/dist/api/auth-handler.js +5 -3
  3. package/dist/api-handlers/account/change-password.js +6 -3
  4. package/dist/api-handlers/account/masked-info.js +2 -1
  5. package/dist/api-handlers/account/recovery/forward.d.ts +14 -0
  6. package/dist/api-handlers/account/recovery/forward.js +105 -0
  7. package/dist/api-handlers/account/recovery/initiate.js +4 -0
  8. package/dist/api-handlers/account/recovery/send-code.d.ts +12 -2
  9. package/dist/api-handlers/account/recovery/send-code.js +11 -84
  10. package/dist/api-handlers/account/recovery/verify-code.d.ts +11 -2
  11. package/dist/api-handlers/account/recovery/verify-code.js +10 -84
  12. package/dist/api-handlers/account/reset-password.js +24 -1
  13. package/dist/api-handlers/account/send-code.d.ts +1 -15
  14. package/dist/api-handlers/account/send-code.js +3 -6
  15. package/dist/api-handlers/account/update-phone.js +6 -1
  16. package/dist/api-handlers/account/validate-password.js +3 -1
  17. package/dist/api-handlers/account/verify-email.d.ts +1 -17
  18. package/dist/api-handlers/account/verify-email.js +4 -6
  19. package/dist/api-handlers/account/verify-sms.d.ts +1 -17
  20. package/dist/api-handlers/account/verify-sms.js +4 -6
  21. package/dist/api-handlers/admin/analytics.js +12 -4
  22. package/dist/api-handlers/admin/audit.js +12 -4
  23. package/dist/api-handlers/admin/redis-sessions.js +16 -8
  24. package/dist/api-handlers/admin/sessions.js +14 -6
  25. package/dist/api-handlers/admin/site-logs.js +39 -16
  26. package/dist/api-handlers/admin/stats.js +14 -4
  27. package/dist/api-handlers/admin/users.js +14 -6
  28. package/dist/api-handlers/admin/vibe-data.js +19 -11
  29. package/dist/api-handlers/auth/jwks.js +2 -1
  30. package/dist/api-handlers/auth/login.js +13 -4
  31. package/dist/api-handlers/auth/passkey-signin.js +6 -0
  32. package/dist/api-handlers/auth/refresh.js +38 -11
  33. package/dist/api-handlers/auth/signout.js +2 -1
  34. package/dist/api-handlers/auth/status.js +4 -1
  35. package/dist/api-handlers/auth/update-session.d.ts +25 -4
  36. package/dist/api-handlers/auth/update-session.js +38 -11
  37. package/dist/api-handlers/auth/validate.js +4 -2
  38. package/dist/api-handlers/auth/verify-code.js +3 -2
  39. package/dist/api-handlers/mfa/backup-code-verify.d.ts +1 -18
  40. package/dist/api-handlers/mfa/backup-code-verify.js +4 -6
  41. package/dist/api-handlers/mfa/backup-codes.js +2 -1
  42. package/dist/api-handlers/mfa/methods.js +2 -1
  43. package/dist/api-handlers/mfa/totp-confirm.js +2 -1
  44. package/dist/api-handlers/mfa/totp-enroll.js +2 -1
  45. package/dist/api-handlers/mfa/totp-verify.d.ts +1 -19
  46. package/dist/api-handlers/mfa/totp-verify.js +4 -6
  47. package/dist/api-handlers/passkeys/list.js +2 -1
  48. package/dist/api-handlers/passkeys/manage.js +2 -1
  49. package/dist/api-handlers/passkeys/register-options.js +2 -1
  50. package/dist/api-handlers/passkeys/register-verify.js +2 -1
  51. package/dist/api-handlers/session/refresh-viability.d.ts +4 -2
  52. package/dist/api-handlers/session/refresh-viability.js +10 -7
  53. package/dist/api-handlers/session/viability.js +4 -3
  54. package/dist/api-handlers/test/force-expire.js +5 -3
  55. package/dist/auth/auth-decision.js +2 -1
  56. package/dist/auth/better-auth.d.ts +14 -0
  57. package/dist/auth/better-auth.js +33 -13
  58. package/dist/auth/utils/idp-client.js +2 -1
  59. package/dist/components/admin/DataBrowserTab.js +4 -1
  60. package/dist/components/admin/SessionsTab.js +9 -3
  61. package/dist/components/recovery/RequestedStep.d.ts +11 -0
  62. package/dist/components/recovery/RequestedStep.js +14 -0
  63. package/dist/components/recovery/SetPasswordStep.js +10 -1
  64. package/dist/lib/app-slug.d.ts +21 -0
  65. package/dist/lib/app-slug.js +51 -0
  66. package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
  67. package/dist/lib/ensure-fresh-access-token.js +105 -4
  68. package/dist/lib/idp-client-config.d.ts +20 -0
  69. package/dist/lib/idp-client-config.js +65 -14
  70. package/dist/lib/idp-fetch.js +1 -1
  71. package/dist/lib/password-rules.d.ts +22 -0
  72. package/dist/lib/password-rules.js +43 -0
  73. package/dist/lib/safe-error-response.d.ts +67 -0
  74. package/dist/lib/safe-error-response.js +130 -0
  75. package/dist/lib/session-cookie-shape.d.ts +22 -19
  76. package/dist/lib/session-cookie-shape.js +14 -18
  77. package/dist/lib/session-store.d.ts +8 -1
  78. package/dist/lib/session-store.js +170 -24
  79. package/dist/lib/startup-init.d.ts +6 -0
  80. package/dist/lib/startup-init.js +30 -4
  81. package/dist/lib/token-lifecycle.js +2 -1
  82. package/dist/lib/token-log-id.d.ts +18 -0
  83. package/dist/lib/token-log-id.js +84 -0
  84. package/dist/lib/two-factor-verify-failure.d.ts +45 -0
  85. package/dist/lib/two-factor-verify-failure.js +143 -0
  86. package/dist/middleware/create-middleware.d.ts +2 -1
  87. package/dist/middleware/create-middleware.js +43 -8
  88. package/dist/pages/recovery/page.js +14 -4
  89. package/dist/pages/test-env/JwtInspectPage.js +7 -149
  90. package/dist/pages/test-env/RefreshTokenPage.js +4 -1
  91. package/dist/pages/test-env/redact-credentials.d.ts +6 -0
  92. package/dist/pages/test-env/redact-credentials.js +101 -0
  93. package/dist/pages/verify-code/page.js +2 -1
  94. package/dist/routes/auth/logout.d.ts +2 -1
  95. package/dist/routes/auth/logout.js +4 -2
  96. package/dist/routes/auth/session.d.ts +0 -3
  97. package/dist/routes/auth/session.js +16 -10
  98. package/dist/routes/auth/viability.d.ts +3 -2
  99. package/dist/routes/auth/viability.js +11 -7
  100. package/dist/server/auth.js +10 -6
  101. package/dist/server/decode-session.js +15 -26
  102. package/dist/server/verified-session-token.d.ts +13 -0
  103. package/dist/server/verified-session-token.js +103 -0
  104. package/dist/stores/authStore.js +14 -3
  105. package/dist/types/recovery.d.ts +1 -1
  106. package/dist/utils/api.d.ts +18 -0
  107. package/dist/utils/api.js +51 -9
  108. package/dist/vibe/sessions.js +2 -1
  109. package/package.json +2 -1
  110. package/scripts/verify-release-provenance.mjs +410 -0
  111. package/src/api/auth-handler.ts +5 -3
  112. package/src/api-handlers/account/change-password.ts +6 -3
  113. package/src/api-handlers/account/masked-info.ts +2 -1
  114. package/src/api-handlers/account/recovery/forward.ts +117 -0
  115. package/src/api-handlers/account/recovery/initiate.ts +3 -0
  116. package/src/api-handlers/account/recovery/send-code.ts +13 -99
  117. package/src/api-handlers/account/recovery/verify-code.ts +12 -99
  118. package/src/api-handlers/account/reset-password.ts +30 -1
  119. package/src/api-handlers/account/send-code.ts +3 -9
  120. package/src/api-handlers/account/update-phone.ts +6 -1
  121. package/src/api-handlers/account/validate-password.ts +3 -1
  122. package/src/api-handlers/account/verify-email.ts +4 -9
  123. package/src/api-handlers/account/verify-sms.ts +4 -9
  124. package/src/api-handlers/admin/analytics.ts +13 -5
  125. package/src/api-handlers/admin/audit.ts +13 -5
  126. package/src/api-handlers/admin/redis-sessions.ts +16 -8
  127. package/src/api-handlers/admin/sessions.ts +15 -7
  128. package/src/api-handlers/admin/site-logs.ts +39 -16
  129. package/src/api-handlers/admin/stats.ts +15 -5
  130. package/src/api-handlers/admin/users.ts +15 -7
  131. package/src/api-handlers/admin/vibe-data.ts +20 -12
  132. package/src/api-handlers/auth/jwks.ts +2 -1
  133. package/src/api-handlers/auth/login.ts +13 -4
  134. package/src/api-handlers/auth/passkey-signin.ts +4 -0
  135. package/src/api-handlers/auth/refresh.ts +38 -11
  136. package/src/api-handlers/auth/signout.ts +2 -1
  137. package/src/api-handlers/auth/status.ts +4 -1
  138. package/src/api-handlers/auth/update-session.ts +45 -11
  139. package/src/api-handlers/auth/validate.ts +4 -2
  140. package/src/api-handlers/auth/verify-code.ts +3 -2
  141. package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
  142. package/src/api-handlers/mfa/backup-codes.ts +2 -1
  143. package/src/api-handlers/mfa/methods.ts +2 -1
  144. package/src/api-handlers/mfa/totp-confirm.ts +2 -1
  145. package/src/api-handlers/mfa/totp-enroll.ts +2 -1
  146. package/src/api-handlers/mfa/totp-verify.ts +4 -9
  147. package/src/api-handlers/passkeys/list.ts +2 -1
  148. package/src/api-handlers/passkeys/manage.ts +2 -1
  149. package/src/api-handlers/passkeys/register-options.ts +2 -1
  150. package/src/api-handlers/passkeys/register-verify.ts +2 -1
  151. package/src/api-handlers/session/refresh-viability.ts +10 -7
  152. package/src/api-handlers/session/viability.ts +4 -3
  153. package/src/api-handlers/test/force-expire.ts +5 -3
  154. package/src/auth/auth-decision.ts +2 -1
  155. package/src/auth/better-auth.ts +35 -10
  156. package/src/auth/utils/idp-client.ts +2 -1
  157. package/src/components/admin/DataBrowserTab.tsx +4 -1
  158. package/src/components/admin/SessionsTab.tsx +10 -3
  159. package/src/components/recovery/RequestedStep.tsx +30 -0
  160. package/src/components/recovery/SetPasswordStep.tsx +24 -3
  161. package/src/hooks/useViabilitySession.ts +2 -1
  162. package/src/lib/app-slug.ts +50 -0
  163. package/src/lib/ensure-fresh-access-token.ts +133 -4
  164. package/src/lib/idp-client-config.ts +90 -14
  165. package/src/lib/idp-fetch.ts +1 -1
  166. package/src/lib/password-rules.ts +50 -0
  167. package/src/lib/safe-error-response.ts +126 -0
  168. package/src/lib/session-cookie-shape.ts +23 -22
  169. package/src/lib/session-store.ts +167 -24
  170. package/src/lib/startup-init.ts +29 -4
  171. package/src/lib/token-lifecycle.ts +2 -1
  172. package/src/lib/token-log-id.ts +69 -0
  173. package/src/lib/two-factor-verify-failure.ts +167 -0
  174. package/src/middleware/create-middleware.ts +55 -9
  175. package/src/pages/recovery/page.tsx +21 -4
  176. package/src/pages/test-env/JwtInspectPage.tsx +11 -233
  177. package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
  178. package/src/pages/test-env/redact-credentials.ts +96 -0
  179. package/src/pages/verify-code/page.tsx +2 -1
  180. package/src/routes/auth/logout.ts +4 -2
  181. package/src/routes/auth/session.ts +16 -10
  182. package/src/routes/auth/viability.ts +11 -7
  183. package/src/server/auth.ts +10 -6
  184. package/src/server/decode-session.ts +16 -26
  185. package/src/server/verified-session-token.ts +103 -0
  186. package/src/stores/authStore.ts +16 -5
  187. package/src/types/recovery.ts +1 -1
  188. package/src/utils/api.ts +47 -8
  189. package/src/vibe/sessions.ts +2 -1
@@ -13,6 +13,7 @@ import { getSession } from '../../server/auth';
13
13
  import { getStartupIDPConfig } from '../../lib/startup-init';
14
14
  import { resolveClientKey } from '../../lib/client-key';
15
15
  import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
16
+ import { randomUUID } from 'crypto';
16
17
 
17
18
  interface VibeRequestOptions {
18
19
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -57,7 +58,10 @@ async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean;
57
58
  async function vibeServiceRequest<T = unknown>(
58
59
  endpoint: string,
59
60
  options: VibeRequestOptions
60
- ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
61
+ ): Promise<{ ok: boolean; status: number; data: T | null; error?: string; correlationId?: string }> {
62
+ // PAY-1429: `error` can reach the HTTP caller, so it never carries upstream Vibe text or
63
+ // exception text. Failures return a fixed message + correlationId; the same id is on the
64
+ // server log line, which carries status + bodyLength only (PAY-1424).
61
65
  const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
62
66
  const clientId = process.env.VIBE_CLIENT_ID;
63
67
  const signingKey = process.env.VIBE_HMAC_KEY;
@@ -105,13 +109,17 @@ async function vibeServiceRequest<T = unknown>(
105
109
 
106
110
  if (!res.ok) {
107
111
  const errorText = await res.text();
108
- return { ok: false, status: res.status, data: null, error: errorText };
112
+ const correlationId = randomUUID();
113
+ console.error(`[admin/sessions] Vibe request failed: ${options.method} ${endpoint} - ${res.status}`, { correlationId, bodyLength: errorText.length });
114
+ return { ok: false, status: res.status, data: null, error: `Vibe request failed (HTTP ${res.status})`, correlationId };
109
115
  }
110
116
 
111
117
  const body = await res.json();
112
118
  return { ok: true, status: res.status, data: body };
113
119
  } catch (error) {
114
- return { ok: false, status: 0, data: null, error: String(error) };
120
+ const correlationId = randomUUID();
121
+ console.error(`[admin/sessions] Vibe request exception: ${options.method} ${endpoint}`, { correlationId, error });
122
+ return { ok: false, status: 0, data: null, error: 'Vibe request could not be completed', correlationId };
115
123
  }
116
124
  }
117
125
 
@@ -171,7 +179,7 @@ export function createSessionsHandler(config: AdminSessionsHandlerConfig) {
171
179
  );
172
180
 
173
181
  if (!result.ok) {
174
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
182
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
175
183
  }
176
184
 
177
185
  // Extract sessions from response
@@ -214,7 +222,7 @@ export function createSessionsHandler(config: AdminSessionsHandlerConfig) {
214
222
  );
215
223
 
216
224
  if (!result.ok) {
217
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
225
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
218
226
  }
219
227
 
220
228
  const sessions = result.data?.data || result.data?.documents || [];
@@ -262,7 +270,7 @@ export function createSessionsHandler(config: AdminSessionsHandlerConfig) {
262
270
  );
263
271
 
264
272
  if (!result.ok) {
265
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
273
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
266
274
  }
267
275
 
268
276
  return NextResponse.json({ success: true });
@@ -287,7 +295,7 @@ export function createSessionsHandler(config: AdminSessionsHandlerConfig) {
287
295
  );
288
296
 
289
297
  if (!queryResult.ok) {
290
- return NextResponse.json({ error: queryResult.error }, { status: queryResult.status || 500 });
298
+ return NextResponse.json({ error: queryResult.error, code: 'UPSTREAM_ERROR', correlationId: queryResult.correlationId }, { status: queryResult.status || 500 });
291
299
  }
292
300
 
293
301
  const sessions = queryResult.data?.data || queryResult.data?.documents || [];
@@ -16,6 +16,7 @@ import { NextRequest, NextResponse } from 'next/server';
16
16
  import { getSession } from '../../server/auth';
17
17
  import { getRedis } from '../../lib/redis';
18
18
  import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
19
+ import { randomUUID } from 'crypto';
19
20
  const REDIS_SITE_LOG_KEY = 'vibe:site-logs:pending';
20
21
  const REDIS_LOG_TTL = 7 * 24 * 60 * 60; // 1 week
21
22
 
@@ -106,9 +107,13 @@ export function createSiteLogsHandler(config: SiteLogsHandlerConfig) {
106
107
 
107
108
  if (!response.ok) {
108
109
  const errorText = await response.text();
109
- console.error('[admin/site-logs] Vibe error:', response.status, errorText);
110
+ // PAY-1424: never the raw upstream body in the log sink.
111
+ // PAY-1425: nor in the response - a fixed message + code, and a correlationId that
112
+ // matches this log line.
113
+ const correlationId = randomUUID();
114
+ console.error('[admin/site-logs] Vibe error:', { correlationId, status: response.status, bodyLength: errorText.length });
110
115
  return NextResponse.json(
111
- { error: 'Failed to fetch site logs', details: errorText },
116
+ { error: 'Failed to fetch site logs', code: 'UPSTREAM_ERROR', correlationId },
112
117
  { status: response.status }
113
118
  );
114
119
  }
@@ -116,9 +121,11 @@ export function createSiteLogsHandler(config: SiteLogsHandlerConfig) {
116
121
  const data = await response.json();
117
122
  return NextResponse.json(data);
118
123
  } catch (error: any) {
119
- console.error('[admin/site-logs] Error:', error);
124
+ // PAY-1425: exception text (hosts, URLs, driver messages) stays server-side.
125
+ const correlationId = randomUUID();
126
+ console.error('[admin/site-logs] Error:', { correlationId, error });
120
127
  return NextResponse.json(
121
- { error: error.message || 'Internal error' },
128
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
122
129
  { status: 500 }
123
130
  );
124
131
  }
@@ -192,9 +199,11 @@ export function createSiteLogsHandler(config: SiteLogsHandlerConfig) {
192
199
  note: 'Log will appear after drain service processes it (every 5 seconds)',
193
200
  }, { status: 201 });
194
201
  } catch (error: any) {
195
- console.error('[admin/site-logs] POST Error:', error);
202
+ // PAY-1425: exception text (hosts, URLs, driver messages) stays server-side.
203
+ const correlationId = randomUUID();
204
+ console.error('[admin/site-logs] POST Error:', { correlationId, error });
196
205
  return NextResponse.json(
197
- { error: error.message || 'Internal error' },
206
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
198
207
  { status: 500 }
199
208
  );
200
209
  }
@@ -232,9 +241,13 @@ export function createSiteLogsStatsHandler(config: SiteLogsHandlerConfig) {
232
241
 
233
242
  if (!response.ok) {
234
243
  const errorText = await response.text();
235
- console.error('[admin/site-logs/stats] Vibe error:', response.status, errorText);
244
+ // PAY-1424: never the raw upstream body in the log sink.
245
+ // PAY-1425: nor in the response - a fixed message + code, and a correlationId that
246
+ // matches this log line.
247
+ const correlationId = randomUUID();
248
+ console.error('[admin/site-logs/stats] Vibe error:', { correlationId, status: response.status, bodyLength: errorText.length });
236
249
  return NextResponse.json(
237
- { error: 'Failed to fetch stats', details: errorText },
250
+ { error: 'Failed to fetch stats', code: 'UPSTREAM_ERROR', correlationId },
238
251
  { status: response.status }
239
252
  );
240
253
  }
@@ -243,9 +256,11 @@ export function createSiteLogsStatsHandler(config: SiteLogsHandlerConfig) {
243
256
  // Ensure response matches expected format: { total, unique_users, by_level, by_category }
244
257
  return NextResponse.json(data);
245
258
  } catch (error: any) {
246
- console.error('[admin/site-logs/stats] Error:', error);
259
+ // PAY-1425: exception text (hosts, URLs, driver messages) stays server-side.
260
+ const correlationId = randomUUID();
261
+ console.error('[admin/site-logs/stats] Error:', { correlationId, error });
247
262
  return NextResponse.json(
248
- { error: error.message || 'Internal error' },
263
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
249
264
  { status: 500 }
250
265
  );
251
266
  }
@@ -282,9 +297,13 @@ export function createSiteLogsDrainHandler(config: SiteLogsHandlerConfig) {
282
297
 
283
298
  if (!response.ok) {
284
299
  const errorText = await response.text();
285
- console.error('[admin/site-logs/drain] Vibe error:', response.status, errorText);
300
+ // PAY-1424: never the raw upstream body in the log sink.
301
+ // PAY-1425: nor in the response - a fixed message + code, and a correlationId that
302
+ // matches this log line.
303
+ const correlationId = randomUUID();
304
+ console.error('[admin/site-logs/drain] Vibe error:', { correlationId, status: response.status, bodyLength: errorText.length });
286
305
  return NextResponse.json(
287
- { error: 'Failed to drain site logs', details: errorText },
306
+ { error: 'Failed to drain site logs', code: 'UPSTREAM_ERROR', correlationId },
288
307
  { status: response.status }
289
308
  );
290
309
  }
@@ -292,9 +311,11 @@ export function createSiteLogsDrainHandler(config: SiteLogsHandlerConfig) {
292
311
  const data = await response.json();
293
312
  return NextResponse.json(data);
294
313
  } catch (error: any) {
295
- console.error('[admin/site-logs/drain] Error:', error);
314
+ // PAY-1425: exception text (hosts, URLs, driver messages) stays server-side.
315
+ const correlationId = randomUUID();
316
+ console.error('[admin/site-logs/drain] Error:', { correlationId, error });
296
317
  return NextResponse.json(
297
- { error: error.message || 'Internal error' },
318
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
298
319
  { status: 500 }
299
320
  );
300
321
  }
@@ -354,9 +375,11 @@ export function createSiteLogsQueueHandler(config: SiteLogsHandlerConfig) {
354
375
  status: queueLength === 0 ? 'empty' : 'pending',
355
376
  });
356
377
  } catch (error: any) {
357
- console.error('[admin/site-logs/queue] Error:', error);
378
+ // PAY-1425: exception text (hosts, URLs, driver messages) stays server-side.
379
+ const correlationId = randomUUID();
380
+ console.error('[admin/site-logs/queue] Error:', { correlationId, error });
358
381
  return NextResponse.json(
359
- { error: error.message || 'Internal error' },
382
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
360
383
  { status: 500 }
361
384
  );
362
385
  }
@@ -14,6 +14,7 @@ import { getStartupIDPConfig } from '../../lib/startup-init';
14
14
  import { resolveClientKey } from '../../lib/client-key';
15
15
  import { getRedis } from '../../lib/redis';
16
16
  import { ADMIN_ROLES } from '../../lib/roles';
17
+ import { randomUUID } from 'crypto';
17
18
 
18
19
  interface VibeRequestOptions {
19
20
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -46,7 +47,10 @@ async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean;
46
47
  async function vibeServiceRequest<T = unknown>(
47
48
  endpoint: string,
48
49
  options: VibeRequestOptions
49
- ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
50
+ ): Promise<{ ok: boolean; status: number; data: T | null; error?: string; correlationId?: string }> {
51
+ // PAY-1429: `error` can reach the HTTP caller, so it never carries upstream Vibe text or
52
+ // exception text. Failures return a fixed message + correlationId; the same id is on the
53
+ // server log line, which carries status + bodyLength only (PAY-1424).
50
54
  const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
51
55
  const clientId = process.env.VIBE_CLIENT_ID;
52
56
  const signingKey = process.env.VIBE_HMAC_KEY;
@@ -90,13 +94,17 @@ async function vibeServiceRequest<T = unknown>(
90
94
  if (res.status === 204) return { ok: true, status: 204, data: null };
91
95
  if (!res.ok) {
92
96
  const errorText = await res.text();
93
- return { ok: false, status: res.status, data: null, error: errorText };
97
+ const correlationId = randomUUID();
98
+ console.error(`[admin/stats] Vibe request failed: ${options.method} ${endpoint} - ${res.status}`, { correlationId, bodyLength: errorText.length });
99
+ return { ok: false, status: res.status, data: null, error: `Vibe request failed (HTTP ${res.status})`, correlationId };
94
100
  }
95
101
 
96
102
  const body = await res.json();
97
103
  return { ok: true, status: res.status, data: body };
98
104
  } catch (error) {
99
- return { ok: false, status: 0, data: null, error: String(error) };
105
+ const correlationId = randomUUID();
106
+ console.error(`[admin/stats] Vibe request exception: ${options.method} ${endpoint}`, { correlationId, error });
107
+ return { ok: false, status: 0, data: null, error: 'Vibe request could not be completed', correlationId };
100
108
  }
101
109
  }
102
110
 
@@ -239,9 +247,11 @@ export function createStatsHandler(config: AdminStatsHandlerConfig) {
239
247
  recentActivity,
240
248
  });
241
249
  } catch (error: any) {
242
- console.error('[admin/stats] Error:', error);
250
+ // PAY-1429: exception text (hosts, URLs, driver messages) stays server-side.
251
+ const correlationId = randomUUID();
252
+ console.error('[admin/stats] Error:', { correlationId, error });
243
253
  return NextResponse.json(
244
- { error: error.message || 'Internal error' },
254
+ { error: 'Internal error', code: 'INTERNAL_ERROR', correlationId },
245
255
  { status: 500 }
246
256
  );
247
257
  }
@@ -12,6 +12,7 @@ import { getSession } from '../../server/auth';
12
12
  import { getStartupIDPConfig } from '../../lib/startup-init';
13
13
  import { resolveClientKey } from '../../lib/client-key';
14
14
  import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
15
+ import { randomUUID } from 'crypto';
15
16
 
16
17
  interface VibeRequestOptions {
17
18
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -44,7 +45,10 @@ async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean;
44
45
  async function vibeServiceRequest<T = unknown>(
45
46
  endpoint: string,
46
47
  options: VibeRequestOptions
47
- ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
48
+ ): Promise<{ ok: boolean; status: number; data: T | null; error?: string; correlationId?: string }> {
49
+ // PAY-1429: `error` can reach the HTTP caller, so it never carries upstream Vibe text or
50
+ // exception text. Failures return a fixed message + correlationId; the same id is on the
51
+ // server log line, which carries status + bodyLength only (PAY-1424).
48
52
  const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
49
53
  const clientId = process.env.VIBE_CLIENT_ID;
50
54
  const signingKey = process.env.VIBE_HMAC_KEY;
@@ -89,13 +93,17 @@ async function vibeServiceRequest<T = unknown>(
89
93
  if (res.status === 204) return { ok: true, status: 204, data: null };
90
94
  if (!res.ok) {
91
95
  const errorText = await res.text();
92
- return { ok: false, status: res.status, data: null, error: errorText };
96
+ const correlationId = randomUUID();
97
+ console.error(`[admin/users] Vibe request failed: ${options.method} ${endpoint} - ${res.status}`, { correlationId, bodyLength: errorText.length });
98
+ return { ok: false, status: res.status, data: null, error: `Vibe request failed (HTTP ${res.status})`, correlationId };
93
99
  }
94
100
 
95
101
  const body = await res.json();
96
102
  return { ok: true, status: res.status, data: body };
97
103
  } catch (error) {
98
- return { ok: false, status: 0, data: null, error: String(error) };
104
+ const correlationId = randomUUID();
105
+ console.error(`[admin/users] Vibe request exception: ${options.method} ${endpoint}`, { correlationId, error });
106
+ return { ok: false, status: 0, data: null, error: 'Vibe request could not be completed', correlationId };
99
107
  }
100
108
  }
101
109
 
@@ -151,7 +159,7 @@ export function createUsersHandler(config: AdminUsersHandlerConfig) {
151
159
  );
152
160
 
153
161
  if (!result.ok) {
154
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
162
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
155
163
  }
156
164
 
157
165
  const rawUsers = result.data?.data || result.data?.documents || [];
@@ -187,7 +195,7 @@ export function createUsersHandler(config: AdminUsersHandlerConfig) {
187
195
  );
188
196
 
189
197
  if (!result.ok) {
190
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
198
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
191
199
  }
192
200
 
193
201
  const users = result.data?.data || result.data?.documents || [];
@@ -218,7 +226,7 @@ export function createUsersHandler(config: AdminUsersHandlerConfig) {
218
226
  );
219
227
 
220
228
  if (!result.ok) {
221
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
229
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
222
230
  }
223
231
 
224
232
  return NextResponse.json({ success: true });
@@ -231,7 +239,7 @@ export function createUsersHandler(config: AdminUsersHandlerConfig) {
231
239
  );
232
240
 
233
241
  if (!result.ok) {
234
- return NextResponse.json({ error: result.error }, { status: result.status || 500 });
242
+ return NextResponse.json({ error: result.error, code: 'UPSTREAM_ERROR', correlationId: result.correlationId }, { status: result.status || 500 });
235
243
  }
236
244
 
237
245
  return NextResponse.json({ success: true });
@@ -13,6 +13,7 @@ import { getSession } from '../../server/auth';
13
13
  import { getStartupIDPConfig } from '../../lib/startup-init';
14
14
  import { resolveClientKey } from '../../lib/client-key';
15
15
  import { ADMIN_ROLES, hasAnyRole } from '../../lib/roles';
16
+ import { randomUUID } from 'crypto';
16
17
 
17
18
  interface VibeRequestOptions {
18
19
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -57,7 +58,11 @@ async function checkAdminRole(request: NextRequest): Promise<{ isAdmin: boolean;
57
58
  async function vibeServiceRequest<T = unknown>(
58
59
  endpoint: string,
59
60
  options: VibeRequestOptions
60
- ): Promise<{ ok: boolean; status: number; data: T | null; error?: string }> {
61
+ ): Promise<{ ok: boolean; status: number; data: T | null; error?: string; correlationId?: string }> {
62
+ // PAY-1425: `error` is returned to the HTTP caller verbatim by every handler below, so it
63
+ // must NEVER carry upstream text (raw Vibe/VibeSQL error bodies can hold SQL fragments,
64
+ // schema names or connection detail) or exception text. Failures return a fixed message
65
+ // plus a correlationId; the same id is on the server log line for operators.
61
66
  const idpUrl = process.env.NEXT_PUBLIC_IDP_URL || process.env.IDP_URL;
62
67
  const clientId = process.env.VIBE_CLIENT_ID;
63
68
  const signingKey = process.env.VIBE_HMAC_KEY;
@@ -108,15 +113,18 @@ async function vibeServiceRequest<T = unknown>(
108
113
 
109
114
  if (!res.ok) {
110
115
  const errorText = await res.text();
111
- console.error(`[Admin Vibe] Request failed: ${options.method} ${endpoint} - ${res.status}`, errorText);
112
- return { ok: false, status: res.status, data: null, error: errorText };
116
+ // PAY-1424: never the raw upstream body in the log sink.
117
+ const correlationId = randomUUID();
118
+ console.error(`[Admin Vibe] Request failed: ${options.method} ${endpoint} - ${res.status}`, { correlationId, bodyLength: errorText.length });
119
+ return { ok: false, status: res.status, data: null, error: `Vibe request failed (HTTP ${res.status})`, correlationId };
113
120
  }
114
121
 
115
122
  const body = await res.json();
116
123
  return { ok: true, status: res.status, data: body };
117
124
  } catch (error) {
118
- console.error(`[Admin Vibe] Request exception: ${options.method} ${endpoint}`, error);
119
- return { ok: false, status: 0, data: null, error: String(error) };
125
+ const correlationId = randomUUID();
126
+ console.error(`[Admin Vibe] Request exception: ${options.method} ${endpoint}`, { correlationId, error });
127
+ return { ok: false, status: 0, data: null, error: 'Vibe request could not be completed', correlationId };
120
128
  }
121
129
  }
122
130
 
@@ -140,7 +148,7 @@ export function createGetCollectionsHandler(config: AdminVibeHandlerConfig) {
140
148
 
141
149
  if (!result.ok) {
142
150
  return NextResponse.json(
143
- { success: false, error: { code: 'FETCH_ERROR', message: result.error } },
151
+ { success: false, error: { code: 'FETCH_ERROR', message: result.error, correlationId: result.correlationId } },
144
152
  { status: result.status || 500 }
145
153
  );
146
154
  }
@@ -166,7 +174,7 @@ export function createGetTablesHandler(config: AdminVibeHandlerConfig) {
166
174
 
167
175
  if (!result.ok) {
168
176
  return NextResponse.json(
169
- { success: false, error: { code: 'FETCH_ERROR', message: result.error } },
177
+ { success: false, error: { code: 'FETCH_ERROR', message: result.error, correlationId: result.correlationId } },
170
178
  { status: result.status || 500 }
171
179
  );
172
180
  }
@@ -200,7 +208,7 @@ export function createGetTableDataHandler(config: AdminVibeHandlerConfig) {
200
208
 
201
209
  if (!result.ok) {
202
210
  return NextResponse.json(
203
- { success: false, error: { code: 'FETCH_ERROR', message: result.error } },
211
+ { success: false, error: { code: 'FETCH_ERROR', message: result.error, correlationId: result.correlationId } },
204
212
  { status: result.status || 500 }
205
213
  );
206
214
  }
@@ -228,7 +236,7 @@ export function createGetRecordHandler(config: AdminVibeHandlerConfig) {
228
236
  if (!result.ok) {
229
237
  const status = result.status === 404 ? 404 : result.status || 500;
230
238
  return NextResponse.json(
231
- { success: false, error: { code: result.status === 404 ? 'NOT_FOUND' : 'FETCH_ERROR', message: result.error } },
239
+ { success: false, error: { code: result.status === 404 ? 'NOT_FOUND' : 'FETCH_ERROR', message: result.error, correlationId: result.correlationId } },
232
240
  { status }
233
241
  );
234
242
  }
@@ -256,7 +264,7 @@ export function createUpdateRecordHandler(config: AdminVibeHandlerConfig) {
256
264
 
257
265
  if (!result.ok) {
258
266
  return NextResponse.json(
259
- { success: false, error: { code: 'UPDATE_ERROR', message: result.error } },
267
+ { success: false, error: { code: 'UPDATE_ERROR', message: result.error, correlationId: result.correlationId } },
260
268
  { status: result.status || 500 }
261
269
  );
262
270
  }
@@ -283,7 +291,7 @@ export function createDeleteRecordHandler(config: AdminVibeHandlerConfig) {
283
291
 
284
292
  if (!result.ok) {
285
293
  return NextResponse.json(
286
- { success: false, error: { code: 'DELETE_ERROR', message: result.error } },
294
+ { success: false, error: { code: 'DELETE_ERROR', message: result.error, correlationId: result.correlationId } },
287
295
  { status: result.status || 500 }
288
296
  );
289
297
  }
@@ -315,7 +323,7 @@ export function createQueryHandler(config: AdminVibeHandlerConfig) {
315
323
 
316
324
  if (!result.ok) {
317
325
  return NextResponse.json(
318
- { success: false, error: { code: 'QUERY_ERROR', message: result.error } },
326
+ { success: false, error: { code: 'QUERY_ERROR', message: result.error, correlationId: result.correlationId } },
319
327
  { status: result.status || 500 }
320
328
  );
321
329
  }
@@ -14,7 +14,8 @@ export async function GET(_req: NextRequest) {
14
14
  const json = JSON.parse(bodyText);
15
15
  return NextResponse.json(json, { status: res.status });
16
16
  } catch (e) {
17
- logger.error('[AUTH_JWKS] Upstream returned non-JSON', { status: res.status, bodyText: bodyText?.slice(0, 200) });
17
+ // PAY-1427: no body slice - a non-JSON upstream is an error/proxy page of unknown content.
18
+ logger.error('[AUTH_JWKS] Upstream returned non-JSON', { status: res.status, bodyLength: bodyText.length, contentType: res.headers.get('content-type') });
18
19
  return NextResponse.json({ error: 'Invalid JWKS response' }, { status: 502 });
19
20
  }
20
21
  } catch (error) {
@@ -162,9 +162,12 @@ export function createLoginHandler(config: LoginConfig) {
162
162
  try {
163
163
  data = JSON.parse(rawText);
164
164
  } catch (jsonError) {
165
+ // PAY-1424: never the raw body - a non-JSON IDP/proxy error page can echo request
166
+ // context (credentials) back. Status, length and content-type are enough to triage.
165
167
  console.error('[LOGIN] IDP returned non-JSON response', {
166
- response: rawText,
167
- status: backendResponse.status
168
+ status: backendResponse.status,
169
+ bodyLength: rawText.length,
170
+ contentType: backendResponse.headers.get('content-type'),
168
171
  });
169
172
  return NextResponse.json(
170
173
  { error: 'Invalid response from authentication service' },
@@ -187,16 +190,22 @@ export function createLoginHandler(config: LoginConfig) {
187
190
  }
188
191
 
189
192
  // Pass through error from IDP
193
+ // PAY-1424: shape only, never the envelope contents.
190
194
  console.error('[LOGIN] IDP service error', {
191
195
  status: backendResponse.status,
192
- error: data
196
+ bodyKeys: data && typeof data === 'object' ? Object.keys(data) : undefined,
197
+ errorKeys: data?.error && typeof data?.error === 'object' ? Object.keys(data?.error) : undefined,
193
198
  });
194
199
  return addSecurityHeaders(NextResponse.json(data, { status: backendResponse.status }));
195
200
  }
196
201
 
197
202
  if (!data.success) {
198
203
  // IDP returned a canonical error envelope with HTTP 200
199
- console.error('[LOGIN] IDP authentication failure', { error: data });
204
+ // PAY-1424: shape only, never the envelope contents.
205
+ console.error('[LOGIN] IDP authentication failure', {
206
+ bodyKeys: data && typeof data === 'object' ? Object.keys(data) : undefined,
207
+ errorKeys: data?.error && typeof data?.error === 'object' ? Object.keys(data?.error) : undefined,
208
+ });
200
209
  return addSecurityHeaders(NextResponse.json(data, { status: 200 }));
201
210
  }
202
211
 
@@ -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 { assertAbsoluteIdpUrl } from '../../lib/idp-url-guard';
6
+ import { idpErrorEnvelope } from '../../lib/safe-error-response';
6
7
 
7
8
  /**
8
9
  * Passkey SIGN-IN ceremony (ANONYMOUS — no session exists yet):
@@ -54,6 +55,8 @@ async function anonCall(req: NextRequest, path: string) {
54
55
 
55
56
  export async function optionsPOST(req: NextRequest) {
56
57
  const { res, json } = await anonCall(req, '/api/ExternalAuth/passwordless/passkey/options');
58
+ // PAY-1438: anonymous route - on failure only the whitelisted envelope, never the IDP body.
59
+ if (!res.ok) return NextResponse.json(idpErrorEnvelope(json, 'Passkey sign-in failed'), { status: res.status });
57
60
  return NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
58
61
  }
59
62
 
@@ -108,5 +111,6 @@ export async function verifyPOST(req: NextRequest) {
108
111
  cache: 'no-store',
109
112
  });
110
113
  const json = await res.json().catch(() => null);
114
+ if (!res.ok) return NextResponse.json(idpErrorEnvelope(json, 'Passkey sign-in failed'), { status: res.status });
111
115
  return NextResponse.json(json ?? { success: false, message: 'Empty upstream response' }, { status: res.status });
112
116
  }
@@ -40,6 +40,7 @@ import { getSession as getBetterAuthSession } from '../../server/auth';
40
40
  import { getSession, updateSession, acquireRefreshLock, releaseRefreshLock, checkRefreshLock } from '../../lib/session-store';
41
41
  import { computeTokenExpiries } from '../../lib/token-expiry';
42
42
  import { extractKidFromToken } from '../../auth/utils/token-utils';
43
+ import { tokenLogId } from '../../lib/token-log-id';
43
44
 
44
45
  interface RefreshConfig {
45
46
  idpBaseUrl: string;
@@ -168,7 +169,7 @@ export function createRefreshHandler(config: RefreshConfig) {
168
169
  console.log('║ REFRESH TOKEN ATTEMPT - PRE-FLIGHT ║');
169
170
  console.log('╠══════════════════════════════════════════════════════════════════════════════╣');
170
171
  console.log(`║ User: ${(userId || 'unknown').substring(0, 50).padEnd(50)} ║`);
171
- console.log(`║ Session: ${sessionToken.substring(0, 8)}... ║`);
172
+ console.log(`║ Session: ${tokenLogId(sessionToken)} ║`);
172
173
  console.log('╠──────────────────────────────────────────────────────────────────────────────╣');
173
174
  console.log(`║ Access Token Expires In: ${String(accessTokenExpiresIn).padEnd(10)} seconds ║`);
174
175
  console.log(`║ Refresh Token Age: ${String(refreshTokenAge).padEnd(10)} seconds ║`);
@@ -520,12 +521,19 @@ export function createRefreshHandler(config: RefreshConfig) {
520
521
  console.error(`║ ${whyItFailed.substring(0, 76).padEnd(76)} ║`);
521
522
  console.error('╠──────────────────────────────────────────────────────────────────────────────╣');
522
523
  console.error(`║ User: ${(userId || 'unknown').substring(0, 60).padEnd(60)}║`);
523
- console.error(`║ Session: ${sessionToken.substring(0, 8)}... ║`);
524
+ console.error(`║ Session: ${tokenLogId(sessionToken)} ║`);
524
525
  console.error(`║ Resolution: ${(resolution || 'Check IDP logs').substring(0, 60).padEnd(60)}║`);
525
526
  console.error('╚══════════════════════════════════════════════════════════════════════════════╝');
526
527
 
527
- // Also log the full response for detailed debugging
528
- console.error('[AUTH_REFRESH] Full IDP error response:', JSON.stringify(responseData, null, 2).substring(0, 1000));
528
+ // NEVER the raw envelope (DotNetPert-Scout, PR 139 review / mail 58691): this used to print
529
+ // a stringified dump of responseData (first 1000 chars) - the same whole IDP body the :618
530
+ // "unsuccessful" branch stopped logging. The box above already carries status, code and
531
+ // flags; add envelope SHAPE only (key names), never values.
532
+ console.error('[AUTH_REFRESH] IDP error response shape', {
533
+ bodyKeys: responseData && typeof responseData === 'object' ? Object.keys(responseData) : undefined,
534
+ errorKeys: idpError && typeof idpError === 'object' ? Object.keys(idpError) : undefined,
535
+ errorHasMessage: !!idpError.message,
536
+ });
529
537
 
530
538
  // If IDP signals to discard token, clear it from Redis
531
539
  if (discardToken) {
@@ -541,7 +549,7 @@ export function createRefreshHandler(config: RefreshConfig) {
541
549
  reason: 'IDP_DISCARD_TOKEN',
542
550
  errorCode,
543
551
  userId,
544
- sessionToken: sessionToken.substring(0, 8) + '...',
552
+ sessionToken: tokenLogId(sessionToken),
545
553
  hadRefreshToken: !!currentSession.idpRefreshToken,
546
554
  stack: new Error().stack
547
555
  });
@@ -585,7 +593,16 @@ export function createRefreshHandler(config: RefreshConfig) {
585
593
  Object.prototype.hasOwnProperty.call(responseData, 'meta');
586
594
 
587
595
  if (!isCompliant) {
588
- console.error('[AUTH_REFRESH] Upstream non-compliant IDP response', { bodySample: responseData });
596
+ // NEVER the raw body (DotNetPert-Scout, mail 58678): a non-compliant response has an
597
+ // UNKNOWN shape by definition, so there is no basis to assume it is free of tokens or
598
+ // other IDP-side secrets. Log shape metadata only - same "kind, never value" reasoning
599
+ // as the session-store.ts / refresh.ts REFRESH_TOKEN_REMOVAL fixes in PR 138.
600
+ console.error('[AUTH_REFRESH] Upstream non-compliant IDP response', {
601
+ bodyType: typeof responseData,
602
+ bodyKeys: responseData && typeof responseData === 'object' ? Object.keys(responseData) : undefined,
603
+ hasSuccessKey: !!(responseData && typeof responseData === 'object' && Object.prototype.hasOwnProperty.call(responseData, 'success')),
604
+ hasMetaKey: !!(responseData && typeof responseData === 'object' && Object.prototype.hasOwnProperty.call(responseData, 'meta')),
605
+ });
589
606
  return NextResponse.json({
590
607
  error: 'Upstream non-compliance',
591
608
  code: 'UPSTREAM_SERVICE_ERROR',
@@ -601,10 +618,17 @@ export function createRefreshHandler(config: RefreshConfig) {
601
618
  const discardToken = idpError.discard_token === true;
602
619
  const retryable = idpError.retryable === true;
603
620
 
621
+ // NEVER the raw envelope (DotNetPert-Scout, mail 58678): `responseData` here is the
622
+ // WHOLE IDP response body, and its `error` sub-object's contents are not something this
623
+ // handler controls or has audited - printing it wholesale would ship whatever the IDP
624
+ // decided to put there straight to the log sink. Log the fields THIS handler already
625
+ // extracts and acts on (retryable, plus envelope-shape flags for diagnosis) instead.
604
626
  console.warn('[AUTH_REFRESH] IDP refresh unsuccessful', {
605
627
  code: errorCode,
606
628
  discardToken,
607
- body: responseData
629
+ retryable,
630
+ errorHasMessage: !!idpError.message,
631
+ errorKeys: Object.keys(idpError),
608
632
  });
609
633
 
610
634
  if (discardToken) {
@@ -612,7 +636,7 @@ export function createRefreshHandler(config: RefreshConfig) {
612
636
  reason: 'IDP_SUCCESS_FALSE_DISCARD',
613
637
  errorCode,
614
638
  userId,
615
- sessionToken: sessionToken.substring(0, 8) + '...',
639
+ sessionToken: tokenLogId(sessionToken),
616
640
  hadRefreshToken: !!currentSession.idpRefreshToken,
617
641
  stack: new Error().stack
618
642
  });
@@ -698,9 +722,12 @@ export function createRefreshHandler(config: RefreshConfig) {
698
722
  console.error('[AUTH_REFRESH] ⚠️ REFRESH_TOKEN_REMOVAL: No new refresh token in IDP response', {
699
723
  reason: 'NO_NEW_REFRESH_TOKEN_IN_RESPONSE',
700
724
  userId,
701
- sessionToken: sessionToken.substring(0, 8) + '...',
725
+ sessionToken: tokenLogId(sessionToken),
702
726
  hadRefreshToken: true,
703
- refreshTokenFromResponse: refreshToken,
727
+ // Kind only, never the value (credential). This branch runs when refreshToken is not a
728
+ // non-empty string, so the old raw value was almost always empty - but a token-shaped
729
+ // non-string would have been printed. refreshTokenType below already carries the kind.
730
+ refreshTokenFromResponsePresent: refreshToken !== undefined && refreshToken !== null && refreshToken !== '',
704
731
  refreshTokenType: typeof refreshToken,
705
732
  payloadKeys: Object.keys(payload || {}),
706
733
  stack: new Error().stack
@@ -752,7 +779,7 @@ export function createRefreshHandler(config: RefreshConfig) {
752
779
  expiresAt: new Date(accessTokenExpires).toISOString(),
753
780
  userId,
754
781
  hasNewRefreshToken: hasNewRefresh,
755
- tokenPreview: accessToken ? accessToken.substring(0, 20) + '...' : 'none'
782
+ hasAccessToken: !!accessToken // PAY-1427: was tokenPreview (first 20 chars of the new access token)
756
783
  });
757
784
 
758
785
  } catch (error) {