@payez/next-mvp 4.4.5 → 4.4.7

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 (219) 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 +33 -8
  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 +58 -14
  57. package/dist/auth/better-auth.js +125 -54
  58. package/dist/auth/utils/idp-client.js +2 -1
  59. package/dist/client/better-auth-client.d.ts +101 -956
  60. package/dist/components/admin/DataBrowserTab.js +4 -1
  61. package/dist/components/admin/SessionsTab.js +9 -3
  62. package/dist/components/recovery/RequestedStep.d.ts +11 -0
  63. package/dist/components/recovery/RequestedStep.js +14 -0
  64. package/dist/components/recovery/SetPasswordStep.js +10 -1
  65. package/dist/config/env.d.ts +0 -2
  66. package/dist/config/env.js +0 -2
  67. package/dist/index.d.ts +1 -0
  68. package/dist/index.js +8 -1
  69. package/dist/lib/app-slug.d.ts +21 -0
  70. package/dist/lib/app-slug.js +51 -0
  71. package/dist/lib/ensure-fresh-access-token.d.ts +26 -0
  72. package/dist/lib/ensure-fresh-access-token.js +105 -4
  73. package/dist/lib/idp-client-config.d.ts +20 -0
  74. package/dist/lib/idp-client-config.js +73 -45
  75. package/dist/lib/idp-fetch.js +1 -1
  76. package/dist/lib/never-in-response.d.ts +30 -0
  77. package/dist/lib/never-in-response.js +71 -0
  78. package/dist/lib/password-rules.d.ts +22 -0
  79. package/dist/lib/password-rules.js +43 -0
  80. package/dist/lib/safe-error-response.d.ts +67 -0
  81. package/dist/lib/safe-error-response.js +130 -0
  82. package/dist/lib/session-cookie-shape.d.ts +22 -19
  83. package/dist/lib/session-cookie-shape.js +14 -18
  84. package/dist/lib/session-store.d.ts +8 -1
  85. package/dist/lib/session-store.js +170 -24
  86. package/dist/lib/startup-init.d.ts +6 -0
  87. package/dist/lib/startup-init.js +30 -4
  88. package/dist/lib/token-lifecycle.js +2 -1
  89. package/dist/lib/token-log-id.d.ts +18 -0
  90. package/dist/lib/token-log-id.js +84 -0
  91. package/dist/lib/two-factor-verify-failure.d.ts +78 -0
  92. package/dist/lib/two-factor-verify-failure.js +220 -0
  93. package/dist/middleware/create-middleware.d.ts +4 -1
  94. package/dist/middleware/create-middleware.js +79 -8
  95. package/dist/pages/login/page.js +5 -0
  96. package/dist/pages/recovery/page.js +14 -4
  97. package/dist/pages/test-env/JwtInspectPage.js +7 -149
  98. package/dist/pages/test-env/RefreshTokenPage.js +4 -1
  99. package/dist/pages/test-env/redact-credentials.d.ts +6 -0
  100. package/dist/pages/test-env/redact-credentials.js +101 -0
  101. package/dist/pages/verify-code/page.js +6 -3
  102. package/dist/routes/auth/logout.d.ts +2 -1
  103. package/dist/routes/auth/logout.js +4 -2
  104. package/dist/routes/auth/session.d.ts +0 -3
  105. package/dist/routes/auth/session.js +16 -10
  106. package/dist/routes/auth/settings.js +4 -1
  107. package/dist/routes/auth/viability.d.ts +3 -2
  108. package/dist/routes/auth/viability.js +11 -7
  109. package/dist/server/auth.d.ts +36 -294
  110. package/dist/server/auth.js +32 -31
  111. package/dist/server/decode-session.js +15 -26
  112. package/dist/server/verified-session-token.d.ts +13 -0
  113. package/dist/server/verified-session-token.js +103 -0
  114. package/dist/stores/authStore.js +14 -3
  115. package/dist/types/recovery.d.ts +1 -1
  116. package/dist/utils/api.d.ts +18 -0
  117. package/dist/utils/api.js +51 -9
  118. package/dist/vibe/client.d.ts +132 -16
  119. package/dist/vibe/client.js +288 -82
  120. package/dist/vibe/document-unwrap.d.ts +50 -0
  121. package/dist/vibe/document-unwrap.js +78 -0
  122. package/dist/vibe/errors.d.ts +17 -0
  123. package/dist/vibe/errors.js +21 -1
  124. package/dist/vibe/generic.d.ts +2 -49
  125. package/dist/vibe/generic.js +17 -78
  126. package/dist/vibe/index.d.ts +1 -1
  127. package/dist/vibe/index.js +2 -1
  128. package/dist/vibe/sessions.js +2 -1
  129. package/package.json +13 -2
  130. package/scripts/verify-release-provenance.mjs +410 -0
  131. package/src/api/auth-handler.ts +5 -3
  132. package/src/api-handlers/account/change-password.ts +36 -8
  133. package/src/api-handlers/account/masked-info.ts +2 -1
  134. package/src/api-handlers/account/recovery/forward.ts +117 -0
  135. package/src/api-handlers/account/recovery/initiate.ts +3 -0
  136. package/src/api-handlers/account/recovery/send-code.ts +13 -99
  137. package/src/api-handlers/account/recovery/verify-code.ts +12 -99
  138. package/src/api-handlers/account/reset-password.ts +30 -1
  139. package/src/api-handlers/account/send-code.ts +3 -9
  140. package/src/api-handlers/account/update-phone.ts +6 -1
  141. package/src/api-handlers/account/validate-password.ts +3 -1
  142. package/src/api-handlers/account/verify-email.ts +4 -9
  143. package/src/api-handlers/account/verify-sms.ts +4 -9
  144. package/src/api-handlers/admin/analytics.ts +13 -5
  145. package/src/api-handlers/admin/audit.ts +13 -5
  146. package/src/api-handlers/admin/redis-sessions.ts +16 -8
  147. package/src/api-handlers/admin/sessions.ts +15 -7
  148. package/src/api-handlers/admin/site-logs.ts +39 -16
  149. package/src/api-handlers/admin/stats.ts +15 -5
  150. package/src/api-handlers/admin/users.ts +15 -7
  151. package/src/api-handlers/admin/vibe-data.ts +20 -12
  152. package/src/api-handlers/auth/jwks.ts +2 -1
  153. package/src/api-handlers/auth/login.ts +13 -4
  154. package/src/api-handlers/auth/passkey-signin.ts +4 -0
  155. package/src/api-handlers/auth/refresh.ts +38 -11
  156. package/src/api-handlers/auth/signout.ts +2 -1
  157. package/src/api-handlers/auth/status.ts +4 -1
  158. package/src/api-handlers/auth/update-session.ts +45 -11
  159. package/src/api-handlers/auth/validate.ts +4 -2
  160. package/src/api-handlers/auth/verify-code.ts +3 -2
  161. package/src/api-handlers/mfa/backup-code-verify.ts +4 -9
  162. package/src/api-handlers/mfa/backup-codes.ts +2 -1
  163. package/src/api-handlers/mfa/methods.ts +2 -1
  164. package/src/api-handlers/mfa/totp-confirm.ts +2 -1
  165. package/src/api-handlers/mfa/totp-enroll.ts +2 -1
  166. package/src/api-handlers/mfa/totp-verify.ts +4 -9
  167. package/src/api-handlers/passkeys/list.ts +2 -1
  168. package/src/api-handlers/passkeys/manage.ts +2 -1
  169. package/src/api-handlers/passkeys/register-options.ts +2 -1
  170. package/src/api-handlers/passkeys/register-verify.ts +2 -1
  171. package/src/api-handlers/session/refresh-viability.ts +10 -7
  172. package/src/api-handlers/session/viability.ts +4 -3
  173. package/src/api-handlers/test/force-expire.ts +5 -3
  174. package/src/auth/auth-decision.ts +2 -1
  175. package/src/auth/better-auth.ts +128 -61
  176. package/src/auth/utils/idp-client.ts +2 -1
  177. package/src/components/admin/DataBrowserTab.tsx +4 -1
  178. package/src/components/admin/SessionsTab.tsx +10 -3
  179. package/src/components/recovery/RequestedStep.tsx +30 -0
  180. package/src/components/recovery/SetPasswordStep.tsx +24 -3
  181. package/src/config/env.ts +0 -2
  182. package/src/hooks/useViabilitySession.ts +2 -1
  183. package/src/index.ts +4 -0
  184. package/src/lib/app-slug.ts +50 -0
  185. package/src/lib/ensure-fresh-access-token.ts +133 -4
  186. package/src/lib/idp-client-config.ts +98 -45
  187. package/src/lib/idp-fetch.ts +1 -1
  188. package/src/lib/never-in-response.ts +64 -0
  189. package/src/lib/password-rules.ts +50 -0
  190. package/src/lib/safe-error-response.ts +126 -0
  191. package/src/lib/session-cookie-shape.ts +23 -22
  192. package/src/lib/session-store.ts +167 -24
  193. package/src/lib/startup-init.ts +29 -4
  194. package/src/lib/token-lifecycle.ts +2 -1
  195. package/src/lib/token-log-id.ts +69 -0
  196. package/src/lib/two-factor-verify-failure.ts +248 -0
  197. package/src/middleware/create-middleware.ts +97 -9
  198. package/src/pages/login/page.tsx +4 -0
  199. package/src/pages/recovery/page.tsx +21 -4
  200. package/src/pages/test-env/JwtInspectPage.tsx +11 -233
  201. package/src/pages/test-env/RefreshTokenPage.tsx +9 -4
  202. package/src/pages/test-env/redact-credentials.ts +96 -0
  203. package/src/pages/verify-code/page.tsx +6 -3
  204. package/src/routes/auth/logout.ts +4 -2
  205. package/src/routes/auth/session.ts +16 -10
  206. package/src/routes/auth/settings.ts +5 -1
  207. package/src/routes/auth/viability.ts +11 -7
  208. package/src/server/auth.ts +33 -35
  209. package/src/server/decode-session.ts +16 -26
  210. package/src/server/verified-session-token.ts +103 -0
  211. package/src/stores/authStore.ts +16 -5
  212. package/src/types/recovery.ts +1 -1
  213. package/src/utils/api.ts +47 -8
  214. package/src/vibe/client.ts +378 -91
  215. package/src/vibe/document-unwrap.ts +103 -0
  216. package/src/vibe/errors.ts +24 -0
  217. package/src/vibe/generic.ts +8 -100
  218. package/src/vibe/index.ts +1 -0
  219. package/src/vibe/sessions.ts +2 -1
@@ -5,6 +5,7 @@ exports.JwtInspectPage = JwtInspectPage;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const better_auth_client_1 = require("../../client/better-auth-client");
7
7
  const react_1 = require("react");
8
+ const redact_credentials_1 = require("./redact-credentials");
8
9
  // Decode JWT header (contains kid, alg, typ)
9
10
  function decodeJwtHeader(token) {
10
11
  try {
@@ -74,7 +75,6 @@ function JwtInspectPage() {
74
75
  const { data: sessionData, isPending } = better_auth_client_1.authClient.useSession();
75
76
  const session = sessionData;
76
77
  const status = isPending ? 'loading' : session ? 'authenticated' : 'unauthenticated';
77
- const [copied, setCopied] = (0, react_1.useState)(null);
78
78
  const [isDarkMode, setIsDarkMode] = (0, react_1.useState)(false);
79
79
  const [jwtHeader, setJwtHeader] = (0, react_1.useState)(null);
80
80
  const [jwtPayload, setJwtPayload] = (0, react_1.useState)(null);
@@ -111,115 +111,10 @@ function JwtInspectPage() {
111
111
  setJwtPayload(decodeJwtPayload(token));
112
112
  }
113
113
  }, [serverSession, session]);
114
- // 248991 item 3 (QAPert 35021) - CREDENTIALS ARE REDACTED UNTIL DELIBERATELY
115
- // REVEALED.
116
- //
117
- // Before this, loading the page put a live session token on screen and one
118
- // click from the clipboard. That is the precondition the 246007 finding needs
119
- // ("a caller who knows a session token can act on a session it does not own"),
120
- // so this page was the supply side of the attack the same package was being
121
- // hardened against. A screen-share, a screenshot or a support call was enough.
122
- //
123
- // The fix is deliberately NOT removing the tokens or the copy button: replaying
124
- // a bearer in curl is the entire reason this tool exists, and a debug tool that
125
- // cannot show you the thing you are debugging just gets replaced by a worse
126
- // ad-hoc one. What changes is that exposure now requires an INTENTIONAL ACT per
127
- // token, so the default state of a loaded page - the state a bystander, a
128
- // screen-share or a screenshot captures - carries no usable credential.
129
- //
130
- // NOTE: this is a UI affordance, not an access control. Anyone who can load
131
- // this page can still click reveal, and the values are in the network response
132
- // either way. It shrinks accidental disclosure; it does not gate the route.
133
- // The gate is mounting - see this file's docstring and index.ts.
134
- const [revealed, setRevealed] = (0, react_1.useState)({});
135
- const maskToken = (value) => {
136
- if (value.length <= 12)
137
- return '\u2022'.repeat(Math.max(value.length, 8));
138
- return `${value.slice(0, 6)}${'\u2022'.repeat(12)}${value.slice(-4)} (${value.length} chars, hidden)`;
139
- };
140
- // Keys whose STRING values are live credentials. Anchored at the END of the key
141
- // name on purpose: it catches sessionToken / accessToken / refreshToken /
142
- // idpRefreshToken, while deliberately NOT catching the metadata beside them -
143
- // accessTokenExpires, idpAccessTokenExpires, tokenType - which are safe and are
144
- // the fields you actually want when debugging an expiry problem.
145
- const CREDENTIAL_KEY = /(token|secret|password|apikey|credential)$/i;
146
- // VALUE-SHAPE checks, because a key-name sweep has a blind spot that this page
147
- // is squarely in: a credential under an innocuous key. Scout hit the same class
148
- // elsewhere today (35297) - a postgres URL whose only key-name hit was a
149
- // POINTER to the secret, while the actual password sat in the URL value and the
150
- // sweep scored the file SAFE. Here the equivalents are a raw JWT parked under
151
- // `jwt` / `bearer` / `authorization` / `cookie` - none of which end in `token`.
152
- //
153
- // JWT: three base64url segments. The length floors matter - without them
154
- // "4.1.42" matches the three-segment shape and the package version would render
155
- // as a redacted credential, which teaches people the redactor is broken.
156
- const looksLikeJwt = (v) => {
157
- if (v.length < 40)
158
- return false;
159
- const parts = v.split('.');
160
- return parts.length === 3 && parts.every((p) => p.length >= 8 && /^[A-Za-z0-9_-]+$/.test(p));
161
- };
162
- // scheme://user:password@host - redact ONLY the password and keep the rest, so
163
- // the URL stays usable for debugging. Masking the whole value would hide which
164
- // host you are pointed at, which is usually the thing you opened this page for.
165
- const URL_WITH_USERINFO = /^([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/i;
166
- // Third value-shape check, on DotNetPert-Scout's 251951 rule: "a content-pattern
167
- // sweep CANNOT find a credential that has no pattern." A PEM block defeats BOTH
168
- // checks above and the key-name check as well - it has no key=value pair, its key
169
- // name is usually something innocuous like `cert`, `pem`, `chain` or `data`, it is
170
- // not three base64url segments so looksLikeJwt rejects it, and it is not a URL.
171
- // He found a live wildcard TLS private key that had sat committed for 215 days for
172
- // exactly this reason: it was never in anyone's search space, missed by
173
- // construction rather than by carelessness.
174
- //
175
- // HONEST SCOPE: this is defence-in-depth, not a measured exposure. I have not seen
176
- // PEM material in a session object and would not expect to. It is here because this
177
- // dump renders WHATEVER the session carries, and the entire lesson of the two
178
- // previous hardenings on this function is that the next credential arrives in a
179
- // shape the current checks were not written for. Matching the armour band rather
180
- // than the payload costs one regex.
181
- // Matches the PEM ARMOUR BAND itself rather than enumerating key types. A first
182
- // draft listed PRIVATE KEY / CERTIFICATE / RSA / EC / OPENSSH / PGP and a test
183
- // caught it missing plain "-----BEGIN CERTIFICATE-----", because that header has
184
- // no space before the closing dashes while "-----BEGIN RSA PRIVATE KEY-----" does.
185
- // Enumerating the payload types is the same mistake as enumerating credential key
186
- // names, one level down: it only ever covers the shapes already thought of.
187
- // Requiring an alphanumeric after "BEGIN " keeps prose containing the word, and a
188
- // bare "-----BEGIN-----", from matching.
189
- const PEM_BLOCK = /-----BEGIN [A-Z0-9][A-Z0-9 ]*-----/;
190
- // 248991 item 3, SECOND HALF. The masked fields above were only half the fix:
191
- // the Raw Session Data block below re-rendered the SAME tokens in plain text via
192
- // JSON.stringify, one click away behind a neutral "Click to expand" summary - and
193
- // with them idpRefreshToken, which has NO other raw surface on this page (it is
194
- // rendered as a boolean at the Refresh Token row precisely because it is the
195
- // longest-lived credential of the three). Redacting per-field while dumping the
196
- // whole object verbatim is not a partial fix, it is the appearance of one.
197
- const redactCredentials = (value, keyName = '') => {
198
- if (Array.isArray(value))
199
- return value.map((v) => redactCredentials(v));
200
- if (value && typeof value === 'object') {
201
- return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactCredentials(v, k)]));
202
- }
203
- if (typeof value !== 'string' || value.length === 0)
204
- return value;
205
- if (CREDENTIAL_KEY.test(keyName))
206
- return maskToken(value);
207
- // Key-name matching alone is not enough (DotNetPert-Scout 35297): a credential
208
- // can sit under a key that is not credential-shaped at all. Two value-shape
209
- // checks close the cases that actually reach a session object:
210
- if (looksLikeJwt(value))
211
- return maskToken(value);
212
- if (PEM_BLOCK.test(value))
213
- return '••• PEM block redacted (' + value.length + ' chars) •••';
214
- if (URL_WITH_USERINFO.test(value))
215
- return value.replace(URL_WITH_USERINFO, '$1$2:•••••@');
216
- return value;
217
- };
218
- const copyToClipboard = (text, label) => {
219
- navigator.clipboard.writeText(text);
220
- setCopied(label);
221
- setTimeout(() => setCopied(null), 2000);
222
- };
114
+ // PAY-1470 (BAPert rulings 59022, 59040): credentials are shown as tokenLogId + length ONLY. The Reveal toggles
115
+ // (per token and for the raw session JSON) and the Copy buttons are gone: "never renders token material"
116
+ // includes click-to-reveal, because a full bearer one click away on a prod-reachable page is the same defect.
117
+ // The redaction rules (key names, JWT / PEM / URL-password shapes) live in ./redact-credentials.
223
118
  if (status === 'loading') {
224
119
  return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: "Loading session..." }));
225
120
  }
@@ -257,21 +152,9 @@ function JwtInspectPage() {
257
152
  const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
258
153
  const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
259
154
  const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
260
- return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-4xl mx-auto space-y-6", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold", children: "Session Inspector" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${labelClass}`, children: "Better Auth session + IDP tokens from Redis" }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "User Identity" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "User ID", value: user.id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Email", value: user.email, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Name", value: user.name, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "OAuth Provider", value: user.oauthProvider, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "IDP Client ID", value: user.idpClientId, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Merchant ID", value: user.merchantId, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Roles" }), user.roles && user.roles.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-2", children: user.roles.map((role) => ((0, jsx_runtime_1.jsx)("span", { className: `px-3 py-1 rounded-full text-sm font-medium ${isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'}`, children: role }, role))) })) : ((0, jsx_runtime_1.jsx)("p", { className: labelClass, children: "No roles assigned" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "2FA Status" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "2FA Verified:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.twoFactorSessionVerified, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Requires 2FA:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.requiresTwoFactor, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode, invertColors: true })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Methods (AMR)", value: user.authenticationMethods?.join(', '), labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Level (ACR)", value: user.authenticationLevel, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Completed At", value: user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Expires At", value: user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Tokens" }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Session Token (Redis Key):" }), displayExt.sessionToken && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(RevealButton, { revealed: !!revealed.session, onClick: () => setRevealed((r) => ({ ...r, session: !r.session })), isDarkMode: isDarkMode }), revealed.session && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(displayExt.sessionToken, 'session'), copied: copied === 'session', isDarkMode: isDarkMode }))] }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.sessionToken
261
- ? revealed.session
262
- ? displayExt.sessionToken
263
- : maskToken(displayExt.sessionToken)
264
- : 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Access Token (IDP):" }), displayExt.accessToken && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(RevealButton, { revealed: !!revealed.access, onClick: () => setRevealed((r) => ({ ...r, access: !r.access })), isDarkMode: isDarkMode }), revealed.access && ((0, jsx_runtime_1.jsx)(CopyButton, { onClick: () => copyToClipboard(displayExt.accessToken, 'access'), copied: copied === 'access', isDarkMode: isDarkMode }))] }))] }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.accessToken
265
- ? revealed.access
266
- ? displayExt.accessToken
267
- : maskToken(displayExt.accessToken)
268
- : 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Has Refresh Token:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: !!displayExt.refreshToken, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Access Token Expires", value: displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] })] }), jwtHeader && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Header" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Algorithm (alg)", value: jwtHeader.alg, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Type (typ)", value: jwtHeader.typ, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Key ID (kid):" }), ' ', (0, jsx_runtime_1.jsx)("span", { className: `font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`, children: jwtHeader.kid || 'NOT PRESENT' })] }), Object.entries(jwtHeader)
155
+ return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen p-8 ${isDarkMode ? 'bg-slate-950 text-white' : 'bg-gray-50 text-gray-900'}`, children: (0, jsx_runtime_1.jsxs)("div", { className: "max-w-4xl mx-auto space-y-6", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold", children: "Session Inspector" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${labelClass}`, children: "Better Auth session + IDP tokens from Redis" }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "User Identity" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "User ID", value: user.id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Email", value: user.email, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Name", value: user.name, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "OAuth Provider", value: user.oauthProvider, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "IDP Client ID", value: user.idpClientId, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Merchant ID", value: user.merchantId, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-purple-900/30 border border-purple-700' : 'bg-purple-50 border border-purple-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Roles" }), user.roles && user.roles.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "flex flex-wrap gap-2", children: user.roles.map((role) => ((0, jsx_runtime_1.jsx)("span", { className: `px-3 py-1 rounded-full text-sm font-medium ${isDarkMode ? 'bg-purple-800 text-purple-100' : 'bg-purple-200 text-purple-800'}`, children: role }, role))) })) : ((0, jsx_runtime_1.jsx)("p", { className: labelClass, children: "No roles assigned" }))] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-yellow-900/30 border border-yellow-700' : 'bg-yellow-50 border border-yellow-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "2FA Status" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "2FA Verified:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.twoFactorSessionVerified, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Requires 2FA:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: user.requiresTwoFactor, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode, invertColors: true })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Methods (AMR)", value: user.authenticationMethods?.join(', '), labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Auth Level (ACR)", value: user.authenticationLevel, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Completed At", value: user.mfaCompletedAt ? new Date(user.mfaCompletedAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "MFA Expires At", value: user.mfaExpiresAt ? new Date(user.mfaExpiresAt).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-blue-900/30 border border-blue-700' : 'bg-blue-50 border border-blue-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Tokens" }), (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-between mb-2", children: (0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Session Token (Redis Key):" }) }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.sessionToken ? (0, redact_credentials_1.maskToken)(displayExt.sessionToken) : 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("div", { className: "flex items-center justify-between mb-2", children: (0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Access Token (IDP):" }) }), (0, jsx_runtime_1.jsx)("code", { className: `block p-2 rounded text-xs break-all max-h-24 overflow-auto ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: displayExt.accessToken ? (0, redact_credentials_1.maskToken)(displayExt.accessToken) : 'N/A' })] }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Has Refresh Token:" }), ' ', (0, jsx_runtime_1.jsx)(StatusBadge, { value: !!displayExt.refreshToken, trueText: "Yes", falseText: "No", isDarkMode: isDarkMode })] }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Access Token Expires", value: displayExt.accessTokenExpires ? new Date(displayExt.accessTokenExpires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass })] })] })] }), jwtHeader && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-orange-900/30 border border-orange-700' : 'bg-orange-50 border border-orange-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Header" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Algorithm (alg)", value: jwtHeader.alg, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Type (typ)", value: jwtHeader.typ, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: labelClass, children: "Key ID (kid):" }), ' ', (0, jsx_runtime_1.jsx)("span", { className: `font-mono ${jwtHeader.kid ? (isDarkMode ? 'text-green-400' : 'text-green-600') : (isDarkMode ? 'text-red-400' : 'text-red-600')}`, children: jwtHeader.kid || 'NOT PRESENT' })] }), Object.entries(jwtHeader)
269
156
  .filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
270
- .map(([key, value]) => ((0, jsx_runtime_1.jsx)(InfoRow, { label: key, value: typeof value === 'object' ? JSON.stringify(value) : String(value), labelClass: labelClass, valueClass: valueClass }, key)))] })] })), jwtPayload && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Payload Claims" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Subject (sub)", value: jwtPayload.sub, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issuer (iss)", value: jwtPayload.iss, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Audience (aud)", value: Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Client ID", value: jwtPayload.client_id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Expires (exp)", value: jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issued At (iat)", value: jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "AMR (Auth Methods)", value: Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "ACR (Auth Context)", value: jwtPayload.acr, labelClass: labelClass, valueClass: valueClass })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Session Metadata" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Session Expires", value: session?.expires ? new Date(session.expires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Error", value: ext.error, labelClass: labelClass, valueClass: ext.error ? 'text-red-500' : valueClass })] })] }), (0, jsx_runtime_1.jsxs)("details", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("summary", { className: "font-semibold cursor-pointer", children: "Raw Session Data \u2014 credentials redacted (Click to expand)" }), (0, jsx_runtime_1.jsxs)("div", { className: "mt-3 flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)(RevealButton, { revealed: !!revealed.raw, onClick: () => setRevealed((r) => ({ ...r, raw: !r.raw })), isDarkMode: isDarkMode }), (0, jsx_runtime_1.jsx)("span", { className: `text-xs ${labelClass}`, children: revealed.raw
271
- ? 'Showing LIVE credentials in plain text, including the refresh token.'
272
- : 'Token values are masked. Structure, claims and expiry fields are unchanged.' })] }), (0, jsx_runtime_1.jsx)("pre", { className: `mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: JSON.stringify(revealed.raw
273
- ? { clientSession: session, serverSession: serverSession }
274
- : redactCredentials({ clientSession: session, serverSession: serverSession }), null, 2) })] })] }) }));
157
+ .map(([key, value]) => ((0, jsx_runtime_1.jsx)(InfoRow, { label: key, value: typeof value === 'object' ? JSON.stringify((0, redact_credentials_1.redactCredentials)(value)) : String((0, redact_credentials_1.redactCredentials)(value, key)), labelClass: labelClass, valueClass: valueClass }, key)))] })] })), jwtPayload && ((0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${isDarkMode ? 'bg-green-900/30 border border-green-700' : 'bg-green-50 border border-green-200'}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "JWT Payload Claims" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Subject (sub)", value: jwtPayload.sub, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issuer (iss)", value: jwtPayload.iss, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Audience (aud)", value: Array.isArray(jwtPayload.aud) ? jwtPayload.aud.join(', ') : jwtPayload.aud, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Client ID", value: jwtPayload.client_id, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Expires (exp)", value: jwtPayload.exp ? new Date(jwtPayload.exp * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Issued At (iat)", value: jwtPayload.iat ? new Date(jwtPayload.iat * 1000).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "AMR (Auth Methods)", value: Array.isArray(jwtPayload.amr) ? jwtPayload.amr.join(', ') : jwtPayload.amr, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "ACR (Auth Context)", value: jwtPayload.acr, labelClass: labelClass, valueClass: valueClass })] })] })), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-lg font-semibold mb-4", children: "Session Metadata" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-3 text-sm", children: [(0, jsx_runtime_1.jsx)(InfoRow, { label: "Session Expires", value: session?.expires ? new Date(session.expires).toISOString() : undefined, labelClass: labelClass, valueClass: valueClass }), (0, jsx_runtime_1.jsx)(InfoRow, { label: "Error", value: ext.error, labelClass: labelClass, valueClass: ext.error ? 'text-red-500' : valueClass })] })] }), (0, jsx_runtime_1.jsxs)("details", { className: `p-4 rounded-lg ${cardClass}`, children: [(0, jsx_runtime_1.jsx)("summary", { className: "font-semibold cursor-pointer", children: "Raw Session Data \u2014 credentials redacted (Click to expand)" }), (0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: (0, jsx_runtime_1.jsx)("span", { className: `text-xs ${labelClass}`, children: "Token values are masked. Structure, claims and expiry fields are unchanged." }) }), (0, jsx_runtime_1.jsx)("pre", { className: `mt-4 text-xs overflow-auto p-3 rounded ${isDarkMode ? 'bg-slate-800' : 'bg-gray-100'}`, children: JSON.stringify((0, redact_credentials_1.redactCredentials)({ clientSession: session, serverSession: serverSession }), null, 2) })] })] }) }));
275
158
  }
276
159
  // Helper Components
277
160
  function InfoRow({ label, value, labelClass, valueClass, }) {
@@ -284,29 +167,4 @@ function StatusBadge({ value, trueText, falseText, isDarkMode, invertColors = fa
284
167
  : isDarkMode ? 'bg-red-800 text-red-100' : 'bg-red-200 text-red-800';
285
168
  return ((0, jsx_runtime_1.jsx)("span", { className: `px-2 py-0.5 rounded text-xs font-medium ${colorClass}`, children: value ? trueText : falseText }));
286
169
  }
287
- function CopyButton({ onClick, copied, isDarkMode, }) {
288
- return ((0, jsx_runtime_1.jsx)("button", { onClick: onClick, className: `px-2 py-1 rounded text-xs ${copied
289
- ? 'bg-green-600 text-white'
290
- : isDarkMode
291
- ? 'bg-slate-700 hover:bg-slate-600 text-white'
292
- : 'bg-gray-200 hover:bg-gray-300 text-gray-700'}`, children: copied ? 'Copied!' : 'Copy' }));
293
- }
294
- /**
295
- * Reveal toggle for a redacted credential (248991 item 3).
296
- *
297
- * Deliberately labelled "Reveal token" rather than something neutral like
298
- * "Show": the label is the last chance to tell whoever is about to click it,
299
- * possibly while screen-sharing, that a live credential is about to appear.
300
- */
301
- function RevealButton({ revealed, onClick, isDarkMode, }) {
302
- return ((0, jsx_runtime_1.jsx)("button", { onClick: onClick, title: revealed
303
- ? 'Hide this credential again'
304
- : 'Reveals a LIVE credential in plain text. Not while screen-sharing.', className: `px-2 py-1 rounded text-xs ${revealed
305
- ? isDarkMode
306
- ? 'bg-amber-700 hover:bg-amber-600 text-white'
307
- : 'bg-amber-200 hover:bg-amber-300 text-amber-900'
308
- : isDarkMode
309
- ? 'bg-slate-700 hover:bg-slate-600 text-white'
310
- : 'bg-gray-200 hover:bg-gray-300 text-gray-700'}`, children: revealed ? 'Hide' : 'Reveal token' }));
311
- }
312
170
  exports.default = JwtInspectPage;
@@ -5,6 +5,9 @@ exports.RefreshTokenPage = RefreshTokenPage;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const react_1 = require("react");
7
7
  const better_auth_client_1 = require("../../client/better-auth-client");
8
+ // PAY-1470 (BAPert 59022): never render characters of a token, whatever the ENABLE_TEST_ROUTES gate says.
9
+ // idealresume production mounted this page on 4.1.14 (pre-gate) showing 40 chars of both tokens.
10
+ const redact_credentials_1 = require("./redact-credentials");
8
11
  /**
9
12
  * ⚠️ DEBUG PAGE THAT DRIVES A DESTRUCTIVE ENDPOINT — DO NOT MOUNT IN
10
13
  * PRODUCTION. ⚠️
@@ -104,6 +107,6 @@ function RefreshTokenPage() {
104
107
  const diffSecs = Math.floor((diffMs % 60000) / 1000);
105
108
  return `${date.toLocaleTimeString()} (${diffMins}m ${diffSecs}s remaining)`;
106
109
  };
107
- return ((0, jsx_runtime_1.jsxs)("div", { className: "p-8 max-w-2xl mx-auto bg-gray-900 min-h-screen text-white", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold mb-4", children: "Refresh Token Test" }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-6 rounded border border-blue-500 bg-blue-900/30 p-4", children: [(0, jsx_runtime_1.jsx)("h2", { className: "font-semibold mb-2 text-blue-300", children: "Current Session" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-sm space-y-1 font-mono", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "User:" }), " ", session?.user?.email || "Not logged in"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "2FA Complete:" }), " ", String(session?.user?.twoFactorSessionVerified ?? "unknown")] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Access Token:" }), " ", sessionDetails?.accessToken ? `${sessionDetails.accessToken.substring(0, 40)}...` : "N/A"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Refresh Token:" }), " ", sessionDetails?.refreshToken ? `${sessionDetails.refreshToken.substring(0, 40)}...` : "N/A"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Access Expires:" }), " ", formatExpiry(sessionDetails?.accessTokenExpires)] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 mb-4", children: [(0, jsx_runtime_1.jsx)("button", { className: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50", onClick: handleRefresh, disabled: loading, children: loading ? "Refreshing..." : "Test Refresh Token" }), (0, jsx_runtime_1.jsx)("button", { className: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded disabled:opacity-50", onClick: handleForceExpire, disabled: loading, children: "Force Expire Token" }), (0, jsx_runtime_1.jsx)("button", { className: "bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded", onClick: () => window.location.reload(), children: "Reload Page" })] }), result && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded border border-gray-600 bg-gray-800 p-4", children: [(0, jsx_runtime_1.jsx)("h3", { className: "font-semibold mb-2 text-gray-300", children: "Result:" }), (0, jsx_runtime_1.jsx)("pre", { className: "text-xs overflow-x-auto whitespace-pre-wrap text-green-400", children: JSON.stringify(result, null, 2) })] })), (0, jsx_runtime_1.jsxs)("details", { className: "mt-4", children: [(0, jsx_runtime_1.jsx)("summary", { className: "cursor-pointer text-gray-400 hover:text-white", children: "Raw Session Details" }), (0, jsx_runtime_1.jsx)("pre", { className: "mt-2 text-xs bg-gray-800 p-2 rounded overflow-x-auto text-gray-300", children: JSON.stringify(sessionDetails, null, 2) })] })] }));
110
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "p-8 max-w-2xl mx-auto bg-gray-900 min-h-screen text-white", children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold mb-4", children: "Refresh Token Test" }), (0, jsx_runtime_1.jsxs)("div", { className: "mb-6 rounded border border-blue-500 bg-blue-900/30 p-4", children: [(0, jsx_runtime_1.jsx)("h2", { className: "font-semibold mb-2 text-blue-300", children: "Current Session" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-sm space-y-1 font-mono", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "User:" }), " ", session?.user?.email || "Not logged in"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "2FA Complete:" }), " ", String(session?.user?.twoFactorSessionVerified ?? "unknown")] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Access Token:" }), " ", sessionDetails ? (sessionDetails.hasAccessToken ? "present (server-side only)" : "absent") : "N/A"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Refresh Token:" }), " ", sessionDetails ? (sessionDetails.hasRefreshToken ? "present (server-side only)" : "absent") : "N/A"] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("span", { className: "text-gray-400", children: "Access Expires:" }), " ", formatExpiry(sessionDetails?.accessTokenExpires)] })] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2 mb-4", children: [(0, jsx_runtime_1.jsx)("button", { className: "bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded disabled:opacity-50", onClick: handleRefresh, disabled: loading, children: loading ? "Refreshing..." : "Test Refresh Token" }), (0, jsx_runtime_1.jsx)("button", { className: "bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded disabled:opacity-50", onClick: handleForceExpire, disabled: loading, children: "Force Expire Token" }), (0, jsx_runtime_1.jsx)("button", { className: "bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded", onClick: () => window.location.reload(), children: "Reload Page" })] }), result && ((0, jsx_runtime_1.jsxs)("div", { className: "rounded border border-gray-600 bg-gray-800 p-4", children: [(0, jsx_runtime_1.jsx)("h3", { className: "font-semibold mb-2 text-gray-300", children: "Result:" }), (0, jsx_runtime_1.jsx)("pre", { className: "text-xs overflow-x-auto whitespace-pre-wrap text-green-400", children: JSON.stringify((0, redact_credentials_1.redactCredentials)(result), null, 2) })] })), (0, jsx_runtime_1.jsxs)("details", { className: "mt-4", children: [(0, jsx_runtime_1.jsx)("summary", { className: "cursor-pointer text-gray-400 hover:text-white", children: "Raw Session Details" }), (0, jsx_runtime_1.jsx)("pre", { className: "mt-2 text-xs bg-gray-800 p-2 rounded overflow-x-auto text-gray-300", children: JSON.stringify((0, redact_credentials_1.redactCredentials)(sessionDetails), null, 2) })] })] }));
108
111
  }
109
112
  exports.default = RefreshTokenPage;
@@ -0,0 +1,6 @@
1
+ /** A stable, non-reversible stand-in for a credential: `h:<12 hex> (<n> chars, hidden)`. */
2
+ export declare function maskToken(value: string): string;
3
+ /** Is a string under this key displayable? Credential names lose even when a timestamp suffix matches. */
4
+ export declare function isDisplayableKey(keyName: string): boolean;
5
+ /** A deep copy of `value` in which every string not under a displayable key is replaced by maskToken. */
6
+ export declare function redactCredentials(value: unknown, keyName?: string): unknown;
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.maskToken = maskToken;
4
+ exports.isDisplayableKey = isDisplayableKey;
5
+ exports.redactCredentials = redactCredentials;
6
+ /**
7
+ * redact-credentials - what the test-env debug pages may show of a credential: a stable hash and a length, never
8
+ * characters of the value (PAY-1470, BAPert rulings 59022 and 59040).
9
+ *
10
+ * PAY-1481 item 2 (QAPert, PR 186 review 59083): this was a DENYLIST (keys ending in token/secret/password/...).
11
+ * Opaque credentials under `tokens.access`, `redisSessionId`, `sid`, `authorization`, `cookie`, `backupCodes[]` and
12
+ * `code` passed unmasked. It is now an ALLOWLIST: a string is shown only when its key is a known DISPLAYABLE field
13
+ * (identity, roles, timestamps, provider and flag metadata, standard JWT claims). Every other string - an unknown
14
+ * key, an array item under an unknown key, a top-level string - becomes maskToken. A new field is hidden until
15
+ * someone decides it is displayable, which is the safe direction for a page reachable in production builds.
16
+ *
17
+ * Kept from the denylist era, and applied EVEN under a displayable key, because a credential can sit under an
18
+ * innocent name: a JWT (three base64url segments, length floors so "4.1.42" is not one), a PEM armour band, and the
19
+ * password in scheme://user:password@host. Numbers, booleans and null are never credentials and stay readable.
20
+ *
21
+ * There is no Reveal and no copy. "Never renders token material" includes click-to-reveal (59040).
22
+ */
23
+ const token_log_id_1 = require("../../lib/token-log-id");
24
+ /** A stable, non-reversible stand-in for a credential: `h:<12 hex> (<n> chars, hidden)`. */
25
+ function maskToken(value) {
26
+ return `${(0, token_log_id_1.tokenLogId)(value)} (${value.length} chars, hidden)`;
27
+ }
28
+ /**
29
+ * Keys whose string values are displayable. Exact names (case-insensitive), plus timestamp / flag suffix patterns.
30
+ * An exact listed key wins (tokenType is metadata); otherwise a key naming a credential (CREDENTIAL_NAME) is refused.
31
+ */
32
+ const DISPLAYABLE_KEYS = new Set([
33
+ // identity
34
+ 'id', 'userId', 'user_id', 'sub', 'email', 'name', 'given_name', 'family_name', 'preferred_username', 'image',
35
+ 'picture', 'locale', 'phone_number_verified',
36
+ // authorisation metadata
37
+ 'role', 'roles', 'scope', 'scopes', 'permissions', 'authenticationMethods', 'authenticationLevel', 'amr', 'acr',
38
+ 'oauthProvider', 'provider', 'providerId', 'tokenType', 'token_type', 'type', 'typ', 'alg', 'kid', 'iss', 'aud',
39
+ 'azp', 'jti',
40
+ // client / tenant
41
+ 'clientId', 'client_id', 'idpClientId', 'clientSlug', 'slug', 'tenant', 'tenantId', 'merchantId', 'appSlug',
42
+ // result / status fields of the test-env routes
43
+ 'status', 'statusText', 'success', 'action', 'message', 'error', 'reason', 'outcome', 'source', 'shape', 'mode',
44
+ ].map((k) => k.toLowerCase()));
45
+ /** Timestamps, durations and booleans-as-strings by suffix: expires, accessTokenExpires, mfaCompletedAt, iat... */
46
+ const DISPLAYABLE_SUFFIX = /(expires|_at|iat|exp|nbf|hours|seconds|verified|required|enabled|count|version)$/i;
47
+ /** camelCase timestamps (createdAt, mfaCompletedAt): tested on the ORIGINAL key so `format` / `chat` do not match. */
48
+ const CAMEL_AT = /[a-z0-9]At$/;
49
+ /** A key that names a credential is never displayable, whatever suffix it has (e.g. refreshTokenCreatedAt stays OK
50
+ * because it names a TIME; `authorization`, `cookie`, `sid`, `code` are credentials). */
51
+ const CREDENTIAL_NAME = /(token|secret|password|passwd|apikey|api_key|credential|authorization|cookie|^sid$|session_?id$|^code$|codes?$|otp|pin$|signature|private)/i;
52
+ const URL_WITH_USERINFO = /^([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^/\s@]+)@/i;
53
+ const PEM_BLOCK = /-----BEGIN [A-Z0-9][A-Z0-9 ]*-----/;
54
+ function looksLikeJwt(v) {
55
+ if (v.length < 40)
56
+ return false;
57
+ const parts = v.split('.');
58
+ return parts.length === 3 && parts.every((p) => p.length >= 8 && /^[A-Za-z0-9_-]+$/.test(p));
59
+ }
60
+ /** Is a string under this key displayable? Credential names lose even when a timestamp suffix matches. */
61
+ function isDisplayableKey(keyName) {
62
+ if (!keyName)
63
+ return false;
64
+ const k = keyName.toLowerCase();
65
+ // 1. An exact, deliberately listed field (tokenType is metadata even though it says "token").
66
+ if (DISPLAYABLE_KEYS.has(k))
67
+ return true;
68
+ // 2. A key naming a credential is refused - unless what it names is a TIME about it (accessTokenExpires).
69
+ const namesTime = /(expires|_at|iat|exp|nbf|hours|seconds)$/i.test(k) || CAMEL_AT.test(keyName);
70
+ if (CREDENTIAL_NAME.test(k) && !namesTime)
71
+ return false;
72
+ // 3. Metadata by suffix: timestamps, durations, flags, counts, versions, URLs (userinfo is still masked).
73
+ return DISPLAYABLE_SUFFIX.test(k) || CAMEL_AT.test(keyName) || /url$/i.test(k);
74
+ }
75
+ /** A deep copy of `value` in which every string not under a displayable key is replaced by maskToken. */
76
+ function redactCredentials(value, keyName = '') {
77
+ // Array items inherit the array's key: backupCodes[] items are credentials, roles[] items are not.
78
+ if (Array.isArray(value))
79
+ return value.map((v) => redactCredentials(v, keyName));
80
+ if (value && typeof value === 'object') {
81
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactCredentials(v, k)]));
82
+ }
83
+ // NightHawk (PR 204 review): a numeric OTP / code under a credential-named key is a credential too.
84
+ if (typeof value === 'number' && keyName && !isDisplayableKey(keyName) && CREDENTIAL_NAME.test(keyName)) {
85
+ return maskToken(String(value));
86
+ }
87
+ if (typeof value !== 'string' || value.length === 0)
88
+ return value;
89
+ if (looksLikeJwt(value))
90
+ return maskToken(value);
91
+ if (PEM_BLOCK.test(value))
92
+ return '••• PEM block redacted (' + value.length + ' chars) •••';
93
+ if (!isDisplayableKey(keyName))
94
+ return maskToken(value);
95
+ let shown = value;
96
+ if (URL_WITH_USERINFO.test(shown))
97
+ shown = shown.replace(URL_WITH_USERINFO, '$1$2:•••••@');
98
+ // NightHawk (PR 204 review): a URL's query and fragment carry tokens (?token=, #access_token=). Names stay, values go.
99
+ shown = shown.replace(/([?&#][^=&#\s]+=)[^&#\s]*/g, '$1•••');
100
+ return shown;
101
+ }
@@ -302,7 +302,8 @@ function VerifyCodeForm({ enrollHref }) {
302
302
  sessionStorage.removeItem(VERIFY_IN_PROGRESS_KEY);
303
303
  }
304
304
  const safeCallback = callbackUrl.startsWith('/account-auth/') ? '/dashboard' : callbackUrl;
305
- const errorParam = isJwtExpired ? '&error=SessionExpired' : '';
305
+ // PAY-884: an expired JWT here is the provisional pre-2FA token - say so on the login page.
306
+ const errorParam = isJwtExpired ? '&error=TwoFactorTimeout' : '';
306
307
  router.push(`/account-auth/login?callbackUrl=${encodeURIComponent(safeCallback)}${errorParam}`);
307
308
  }, 1500);
308
309
  return;
@@ -366,7 +367,8 @@ function VerifyCodeForm({ enrollHref }) {
366
367
  sessionStorage.removeItem(VERIFY_IN_PROGRESS_KEY);
367
368
  }
368
369
  const safeCallback = callbackUrl.startsWith('/account-auth/') ? '/dashboard' : callbackUrl;
369
- const errorParam = isJwtExpired ? '&error=SessionExpired' : '';
370
+ // PAY-884: an expired JWT here is the provisional pre-2FA token - say so on the login page.
371
+ const errorParam = isJwtExpired ? '&error=TwoFactorTimeout' : '';
370
372
  router.push(`/account-auth/login?callbackUrl=${encodeURIComponent(safeCallback)}${errorParam}`);
371
373
  }, 1500);
372
374
  return;
@@ -405,7 +407,8 @@ function VerifyCodeForm({ enrollHref }) {
405
407
  if (!updateRes.ok) {
406
408
  console.warn('[2FA] update-session returned non-OK status:', updateRes.status);
407
409
  const errorData = await updateRes.json();
408
- console.warn('[2FA] update-session error:', errorData);
410
+ // PAY-1427: the request carried access_token/refresh_token; log the error envelope's shape, never its contents.
411
+ console.warn('[2FA] update-session error:', { keys: errorData && typeof errorData === 'object' ? Object.keys(errorData) : typeof errorData, code: errorData?.error?.code ?? errorData?.code });
409
412
  }
410
413
  else {
411
414
  console.log('[2FA] Session updated successfully with MFA tokens');
@@ -27,5 +27,6 @@ export declare function POST(req: NextRequest): Promise<NextResponse<{
27
27
  message: string;
28
28
  }> | NextResponse<{
29
29
  error: string;
30
- details: string;
30
+ code: string;
31
+ correlationId: string;
31
32
  }>>;
@@ -21,6 +21,7 @@ const auth_1 = require("../../server/auth");
21
21
  const session_store_1 = require("../../lib/session-store");
22
22
  const app_slug_1 = require("../../lib/app-slug");
23
23
  const site_logger_1 = require("../../lib/site-logger");
24
+ const safe_error_response_1 = require("../../lib/safe-error-response");
24
25
  /**
25
26
  * POST /api/auth/logout - Sign out and clean up session
26
27
  *
@@ -90,10 +91,11 @@ async function POST(req) {
90
91
  return response;
91
92
  }
92
93
  catch (error) {
93
- console.error('[LOGOUT_ROUTE] Error during logout:', error);
94
+ const correlationId = (0, safe_error_response_1.logExceptionWithCorrelation)('[LOGOUT_ROUTE] Error during logout:', error);
94
95
  return server_1.NextResponse.json({
95
96
  error: 'Failed to logout',
96
- details: error instanceof Error ? error.message : 'Unknown error'
97
+ code: 'INTERNAL_ERROR',
98
+ correlationId,
97
99
  }, { status: 500 });
98
100
  }
99
101
  }
@@ -46,7 +46,4 @@ export declare function POST(req: NextRequest): Promise<NextResponse<{
46
46
  }> | NextResponse<{
47
47
  success: boolean;
48
48
  message: string;
49
- }> | NextResponse<{
50
- error: string;
51
- details: string;
52
49
  }>>;
@@ -19,6 +19,8 @@ exports.POST = POST;
19
19
  const server_1 = require("next/server");
20
20
  const auth_1 = require("../../server/auth");
21
21
  const session_store_1 = require("../../lib/session-store");
22
+ const safe_error_response_1 = require("../../lib/safe-error-response");
23
+ const token_log_id_1 = require("../../lib/token-log-id");
22
24
  /**
23
25
  * GET /api/auth/session - Check current session status
24
26
  *
@@ -41,7 +43,7 @@ async function GET(req) {
41
43
  userId: authSession.user?.id,
42
44
  email: authSession.user?.email,
43
45
  name: authSession.user?.name,
44
- redisSessionId: redisSessionId ? redisSessionId.substring(0, 8) + '...' : 'MISSING',
46
+ redisSessionId: redisSessionId ? (0, token_log_id_1.tokenLogId)(redisSessionId) : 'MISSING',
45
47
  });
46
48
  // Fetch full session data from Redis
47
49
  const session = redisSessionId ? await (0, session_store_1.getSession)(redisSessionId) : null;
@@ -66,7 +68,7 @@ async function GET(req) {
66
68
  if (!session) {
67
69
  console.error('[SESSION_ROUTE] Redis session MISS — refusing to report authenticated', {
68
70
  userId: authSession.user?.id,
69
- redisSessionId: redisSessionId ? redisSessionId.substring(0, 8) + '...' : 'MISSING',
71
+ redisSessionId: redisSessionId ? (0, token_log_id_1.tokenLogId)(redisSessionId) : 'MISSING',
70
72
  reason: redisSessionId ? 'session expired or evicted' : 'no session token on auth session',
71
73
  });
72
74
  return server_1.NextResponse.json({});
@@ -93,10 +95,11 @@ async function GET(req) {
93
95
  idpClientId: session?.idpClientId,
94
96
  merchantId: session?.merchantId,
95
97
  },
96
- // Session tokens
97
- sessionToken: redisSessionId,
98
- accessToken: session?.idpAccessToken,
99
- refreshToken: session?.idpRefreshToken,
98
+ // PAY-1481 item 1: NO token material. This body is read by browser JS, so returning the session token and the
99
+ // IdP access/refresh tokens handed them to any script on the page (the refresh token outlives the session).
100
+ // Server routes read tokens from the session store; the UI only needs presence and expiry.
101
+ hasAccessToken: !!session?.idpAccessToken,
102
+ hasRefreshToken: !!session?.idpRefreshToken,
100
103
  accessTokenExpires: session?.idpAccessTokenExpires,
101
104
  expires: authSession.session?.expiresAt
102
105
  ? new Date(authSession.session.expiresAt).toISOString()
@@ -104,10 +107,12 @@ async function GET(req) {
104
107
  });
105
108
  }
106
109
  catch (error) {
107
- console.error('[SESSION_ROUTE] Error checking session:', error);
110
+ // PAY-1438: pre-auth route - the exception text stays in the log, keyed by correlationId.
111
+ const correlationId = (0, safe_error_response_1.logExceptionWithCorrelation)('[SESSION_ROUTE] Error checking session:', error);
108
112
  return server_1.NextResponse.json({
109
113
  error: 'Failed to check session',
110
- details: error instanceof Error ? error.message : 'Unknown error'
114
+ code: 'INTERNAL_ERROR',
115
+ correlationId,
111
116
  }, { status: 500 });
112
117
  }
113
118
  }
@@ -231,10 +236,11 @@ async function POST(req) {
231
236
  });
232
237
  }
233
238
  catch (error) {
234
- console.error('[SESSION_ROUTE] Error updating session:', error);
239
+ const correlationId = (0, safe_error_response_1.logExceptionWithCorrelation)('[SESSION_ROUTE] Error updating session:', error);
235
240
  return server_1.NextResponse.json({
236
241
  error: 'Failed to update session',
237
- details: error instanceof Error ? error.message : 'Unknown error'
242
+ code: 'INTERNAL_ERROR',
243
+ correlationId,
238
244
  }, { status: 500 });
239
245
  }
240
246
  }
@@ -31,6 +31,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
31
31
  exports.GET = GET;
32
32
  const server_1 = require("next/server");
33
33
  const idp_client_config_1 = require("../../lib/idp-client-config");
34
+ const never_in_response_1 = require("../../lib/never-in-response");
34
35
  /**
35
36
  * Fail closed and LOUD. 503 + success:false + a code, and CRUCIALLY no policy
36
37
  * fields at all - not defaults, not partials. A consumer cannot mistake an
@@ -105,5 +106,7 @@ async function GET() {
105
106
  require2FA: authSettings.require2FA,
106
107
  allowed2FAMethods: authSettings.allowed2FAMethods,
107
108
  };
108
- return server_1.NextResponse.json({ success: true, data: settings }, { headers: { 'Cache-Control': 'no-store' } });
109
+ // PAY-1529: the public policy is built from an allowlist above; secretSafeJson is the runtime backstop
110
+ // that answers 500 instead of serving it if a registered secret ever reaches this body anyway.
111
+ return (0, never_in_response_1.secretSafeJson)({ success: true, data: settings }, { headers: { 'Cache-Control': 'no-store' } }, 'GET /api/auth/settings');
109
112
  }
@@ -33,7 +33,7 @@ export declare function GET(req: NextRequest): Promise<NextResponse<{
33
33
  expiresIn: number;
34
34
  hasRefreshToken: boolean;
35
35
  authenticated: boolean;
36
- sessionToken: any;
36
+ sessionTokenId: string | null;
37
37
  tenantRequiresTwoFactor: boolean;
38
38
  userHasCompletedTenantTwoFactorRequirements: boolean;
39
39
  userStillNeedsTwoFactor: boolean;
@@ -50,5 +50,6 @@ export declare function GET(req: NextRequest): Promise<NextResponse<{
50
50
  needsRefresh: boolean;
51
51
  authenticated: boolean;
52
52
  error: string;
53
- details: string;
53
+ code: string;
54
+ correlationId: string;
54
55
  }>>;