@payez/next-mvp 4.1.21 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/POST-INSTALL.md +408 -0
  2. package/README.md +7 -0
  3. package/dist/api-handlers/account/recovery/initiate.js +35 -2
  4. package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
  5. package/dist/api-handlers/account/recovery/send-code.js +35 -2
  6. package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
  7. package/dist/api-handlers/account/recovery/verify-code.js +35 -2
  8. package/dist/api-handlers/account/reset-password.js +35 -2
  9. package/dist/api-handlers/account/validate-password.js +21 -3
  10. package/dist/api-handlers/admin/analytics.js +2 -1
  11. package/dist/api-handlers/admin/audit.js +2 -1
  12. package/dist/api-handlers/admin/sessions.js +2 -1
  13. package/dist/api-handlers/admin/stats.js +2 -1
  14. package/dist/api-handlers/admin/users.js +2 -1
  15. package/dist/api-handlers/admin/vibe-data.js +2 -1
  16. package/dist/api-handlers/auth/jwks.js +10 -0
  17. package/dist/api-handlers/auth/login.js +5 -2
  18. package/dist/api-handlers/auth/passkey-signin.js +16 -1
  19. package/dist/api-handlers/auth/refresh.d.ts +23 -3
  20. package/dist/api-handlers/auth/refresh.js +107 -16
  21. package/dist/api-handlers/auth/signout.js +3 -2
  22. package/dist/api-handlers/auth/verify-code.js +5 -2
  23. package/dist/api-handlers/session/viability.js +27 -1
  24. package/dist/api-handlers/test/force-expire.d.ts +29 -15
  25. package/dist/api-handlers/test/force-expire.js +109 -14
  26. package/dist/auth/better-auth.d.ts +263 -6
  27. package/dist/auth/better-auth.js +437 -59
  28. package/dist/auth/utils/idp-client.js +1 -1
  29. package/dist/client/AuthContext.js +49 -17
  30. package/dist/client/better-auth-client.d.ts +62 -62
  31. package/dist/components/account/UserAvatarMenu.js +12 -0
  32. package/dist/components/auth/ModeAwareLoginPage.js +10 -8
  33. package/dist/components/auth/ModeAwareSignupPage.js +5 -2
  34. package/dist/components/auth/TraditionalAuthSection.js +5 -1
  35. package/dist/hooks/useAuthSettings.js +14 -2
  36. package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
  37. package/dist/hooks/usePublicAuthSettings.js +28 -14
  38. package/dist/lib/client-key.d.ts +61 -0
  39. package/dist/lib/client-key.js +72 -0
  40. package/dist/lib/demo-mode.js +2 -1
  41. package/dist/lib/ensure-fresh-access-token.js +5 -1
  42. package/dist/lib/idp-client-config.d.ts +25 -2
  43. package/dist/lib/idp-client-config.js +381 -36
  44. package/dist/lib/idp-fetch.d.ts +2 -0
  45. package/dist/lib/idp-fetch.js +31 -1
  46. package/dist/lib/idp-url-guard.d.ts +29 -0
  47. package/dist/lib/idp-url-guard.js +38 -0
  48. package/dist/lib/redis.js +77 -5
  49. package/dist/lib/session-cookie-shape.d.ts +69 -0
  50. package/dist/lib/session-cookie-shape.js +126 -0
  51. package/dist/lib/session-store.d.ts +35 -2
  52. package/dist/lib/session-store.js +135 -4
  53. package/dist/lib/single-flight.d.ts +35 -0
  54. package/dist/lib/single-flight.js +30 -0
  55. package/dist/lib/token-lifecycle.js +12 -0
  56. package/dist/middleware/create-middleware.js +12 -0
  57. package/dist/models/SessionModel.d.ts +41 -0
  58. package/dist/models/SessionModel.js +9 -0
  59. package/dist/pages/admin-login/page.d.ts +1 -1
  60. package/dist/pages/admin-login/page.js +5 -41
  61. package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
  62. package/dist/pages/security/EnhancedSecurityPage.js +42 -2
  63. package/dist/pages/security/index.d.ts +1 -0
  64. package/dist/pages/security/index.js +3 -0
  65. package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
  66. package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
  67. package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
  68. package/dist/pages/test-env/JwtInspectPage.js +166 -7
  69. package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
  70. package/dist/pages/test-env/RefreshTokenPage.js +22 -6
  71. package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
  72. package/dist/pages/test-env/TestEnvPage.js +16 -4
  73. package/dist/pages/test-env/guarded.d.ts +4 -0
  74. package/dist/pages/test-env/guarded.js +86 -0
  75. package/dist/pages/test-env/index.d.ts +50 -7
  76. package/dist/pages/test-env/index.js +58 -12
  77. package/dist/routes/auth/logout.js +3 -2
  78. package/dist/routes/auth/refresh.js +27 -1
  79. package/dist/routes/auth/session.d.ts +12 -3
  80. package/dist/routes/auth/session.js +92 -9
  81. package/dist/routes/auth/settings.d.ts +34 -4
  82. package/dist/routes/auth/settings.js +87 -33
  83. package/dist/routes/auth/viability.d.ts +1 -1
  84. package/dist/routes/auth/viability.js +122 -11
  85. package/dist/server/auth-guard.js +22 -15
  86. package/dist/server/auth.d.ts +177 -20
  87. package/dist/server/auth.js +34 -75
  88. package/dist/server/decode-session.js +84 -10
  89. package/dist/types/auth.d.ts +18 -0
  90. package/package.json +969 -958
  91. package/scripts/check-internal-url-usage.sh +73 -73
  92. package/scripts/check-publish-health.mjs +120 -0
  93. package/scripts/dev-broker.ps1 +71 -6
  94. package/scripts/print-client-config.mjs +162 -0
  95. package/scripts/server-only-stub.cjs +4 -0
  96. package/src/api-handlers/account/recovery/initiate.ts +35 -2
  97. package/src/api-handlers/account/recovery/send-code.ts +36 -2
  98. package/src/api-handlers/account/recovery/verify-code.ts +36 -2
  99. package/src/api-handlers/account/reset-password.ts +35 -2
  100. package/src/api-handlers/account/validate-password.ts +22 -3
  101. package/src/api-handlers/admin/analytics.ts +2 -1
  102. package/src/api-handlers/admin/audit.ts +2 -1
  103. package/src/api-handlers/admin/sessions.ts +2 -1
  104. package/src/api-handlers/admin/stats.ts +2 -1
  105. package/src/api-handlers/admin/users.ts +2 -1
  106. package/src/api-handlers/admin/vibe-data.ts +2 -1
  107. package/src/api-handlers/auth/jwks.ts +10 -0
  108. package/src/api-handlers/auth/login.ts +5 -2
  109. package/src/api-handlers/auth/passkey-signin.ts +112 -98
  110. package/src/api-handlers/auth/refresh.ts +112 -15
  111. package/src/api-handlers/auth/signout.ts +4 -3
  112. package/src/api-handlers/auth/verify-code.ts +5 -2
  113. package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
  114. package/src/api-handlers/mfa/backup-codes.ts +41 -41
  115. package/src/api-handlers/mfa/methods.ts +46 -46
  116. package/src/api-handlers/mfa/totp-confirm.ts +42 -42
  117. package/src/api-handlers/mfa/totp-enroll.ts +42 -42
  118. package/src/api-handlers/passkeys/list.ts +31 -31
  119. package/src/api-handlers/passkeys/manage.ts +54 -54
  120. package/src/api-handlers/passkeys/register-options.ts +42 -42
  121. package/src/api-handlers/passkeys/register-verify.ts +41 -41
  122. package/src/api-handlers/session/viability.ts +29 -1
  123. package/src/api-handlers/test/force-expire.ts +118 -13
  124. package/src/auth/better-auth.ts +486 -51
  125. package/src/auth/utils/idp-client.ts +1 -1
  126. package/src/client/AuthContext.tsx +55 -19
  127. package/src/components/account/UserAvatarMenu.tsx +11 -0
  128. package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
  129. package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
  130. package/src/components/auth/TraditionalAuthSection.tsx +5 -1
  131. package/src/hooks/useAuthSettings.ts +14 -2
  132. package/src/hooks/usePublicAuthSettings.ts +38 -16
  133. package/src/lib/client-key.ts +88 -0
  134. package/src/lib/demo-mode.ts +2 -1
  135. package/src/lib/ensure-fresh-access-token.ts +5 -1
  136. package/src/lib/idp-client-config.ts +399 -36
  137. package/src/lib/idp-fetch.ts +36 -1
  138. package/src/lib/idp-url-guard.ts +37 -0
  139. package/src/lib/redis.ts +84 -6
  140. package/src/lib/session-cookie-shape.ts +184 -0
  141. package/src/lib/session-store.ts +152 -6
  142. package/src/lib/single-flight.ts +64 -0
  143. package/src/lib/token-lifecycle.ts +12 -0
  144. package/src/middleware/create-middleware.ts +11 -0
  145. package/src/models/SessionModel.ts +49 -0
  146. package/src/pages/admin-login/page.tsx +17 -127
  147. package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
  148. package/src/pages/security/index.ts +3 -0
  149. package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
  150. package/src/pages/test-env/JwtInspectPage.tsx +235 -20
  151. package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
  152. package/src/pages/test-env/TestEnvPage.tsx +16 -4
  153. package/src/pages/test-env/guarded.tsx +81 -0
  154. package/src/pages/test-env/index.ts +59 -7
  155. package/src/routes/auth/logout.ts +4 -3
  156. package/src/routes/auth/passkey-signin.ts +13 -13
  157. package/src/routes/auth/refresh.ts +30 -1
  158. package/src/routes/auth/session.ts +98 -9
  159. package/src/routes/auth/settings.ts +144 -42
  160. package/src/routes/auth/viability.ts +129 -11
  161. package/src/routes/mfa/backup-code-verify.ts +1 -1
  162. package/src/routes/mfa/backup-codes.ts +10 -10
  163. package/src/routes/mfa/methods.ts +22 -22
  164. package/src/routes/mfa/totp-confirm.ts +10 -10
  165. package/src/routes/mfa/totp-enroll.ts +10 -10
  166. package/src/routes/mfa/totp-verify.ts +14 -14
  167. package/src/routes/passkeys/list.ts +10 -10
  168. package/src/routes/passkeys/manage.ts +23 -23
  169. package/src/routes/passkeys/register-options.ts +10 -10
  170. package/src/routes/passkeys/register-verify.ts +10 -10
  171. package/src/server/auth-guard.ts +24 -17
  172. package/src/server/auth.ts +37 -73
  173. package/src/server/decode-session.ts +95 -11
  174. package/src/templates/instrumentation.ts +21 -2
  175. package/src/types/auth.ts +20 -0
  176. package/dist/lib/auth-secret.d.ts +0 -17
  177. package/dist/lib/auth-secret.js +0 -116
  178. package/src/lib/auth-secret.ts +0 -137
@@ -34,16 +34,41 @@ function decodeJwtPayload(token) {
34
34
  }
35
35
  }
36
36
  /**
37
- * Session Inspector Page
37
+ * ⚠️ DEBUG PAGE THAT DISPLAYS AND COPIES LIVE CREDENTIALS — DO NOT MOUNT
38
+ * IN PRODUCTION. ⚠️
38
39
  *
39
- * Debug page for inspecting session data from Redis.
40
- * Shows user info, roles, 2FA status, and tokens.
40
+ * Session Inspector Page. Debug page for inspecting session data from Redis:
41
+ * shows user info, roles, 2FA status, and TOKENS. It offers
42
+ * COPY-TO-CLIPBOARD buttons for the live SESSION TOKEN and ACCESS TOKEN, and
43
+ * reads them from the consuming app's `/api/internal/session-data` route.
41
44
  *
42
- * Usage in consuming app:
45
+ * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
46
+ * THIS PAGE HAS NO ENV GATE OF ITS OWN. Unlike the force-expire handler
47
+ * (default-denied behind ENABLE_TEST_ROUTES) or the idp-token route (behind
48
+ * ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as the
49
+ * consuming app's own middleware allows. MOUNTING IT IS THE GATE. There is no
50
+ * second line of defence behind it.
51
+ *
52
+ * AND THE CREDENTIAL IT HANDS OVER IS THE PRECONDITION FOR A REAL ATTACK ON
53
+ * THIS SAME PACKAGE. The 246007 finding reduces to "a caller who KNOWS A
54
+ * SESSION TOKEN can act on a session it does not own". The purpose of this
55
+ * page is to put a live session token on the clipboard of whoever loads it. A
56
+ * screen-share, a shoulder-surfed screenshot or a support call is enough.
57
+ * Treat the rendered value as a live credential, because that is what it is.
58
+ *
59
+ * NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
60
+ * attached to the exported declaration. An identical block placed higher in
61
+ * the file does NOT reach the emitted .d.ts — measured, after I made exactly
62
+ * that mistake — so it would warn no consumer at all.
63
+ *
64
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
43
65
  * ```typescript
44
- * // app/test-env/jwt-inspect/page.tsx
66
+ * // app/test-env/jwt-inspect/page.tsx — TEST/DEV ENVIRONMENTS ONLY
45
67
  * export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
46
68
  * ```
69
+ * Gate the whole /test-env segment in your middleware, per environment. If you
70
+ * are not actively debugging token contents, DO NOT MOUNT THIS AT ALL — the
71
+ * safest configuration is the absence of the route.
47
72
  */
48
73
  function JwtInspectPage() {
49
74
  const { data: sessionData, isPending } = better_auth_client_1.authClient.useSession();
@@ -86,6 +111,110 @@ function JwtInspectPage() {
86
111
  setJwtPayload(decodeJwtPayload(token));
87
112
  }
88
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
+ };
89
218
  const copyToClipboard = (text, label) => {
90
219
  navigator.clipboard.writeText(text);
91
220
  setCopied(label);
@@ -128,9 +257,21 @@ function JwtInspectPage() {
128
257
  const cardClass = isDarkMode ? 'bg-slate-900 border border-slate-700' : 'bg-white border border-gray-200';
129
258
  const labelClass = isDarkMode ? 'text-slate-400' : 'text-gray-500';
130
259
  const valueClass = isDarkMode ? 'text-white' : 'text-gray-900';
131
- 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.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 || '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.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 || '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)
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)
132
269
  .filter(([key]) => !['alg', 'typ', 'kid'].includes(key))
133
- .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 (Click to expand)" }), (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({ clientSession: session, serverSession: serverSession }, null, 2) })] })] }) }));
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) })] })] }) }));
134
275
  }
135
276
  // Helper Components
136
277
  function InfoRow({ label, value, labelClass, valueClass, }) {
@@ -150,4 +291,22 @@ function CopyButton({ onClick, copied, isDarkMode, }) {
150
291
  ? 'bg-slate-700 hover:bg-slate-600 text-white'
151
292
  : 'bg-gray-200 hover:bg-gray-300 text-gray-700'}`, children: copied ? 'Copied!' : 'Copy' }));
152
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
+ }
153
312
  exports.default = JwtInspectPage;
@@ -1,15 +1,31 @@
1
1
  /**
2
- * Refresh Token Test Page
2
+ * ⚠️ DEBUG PAGE THAT DRIVES A DESTRUCTIVE ENDPOINT — DO NOT MOUNT IN
3
+ * PRODUCTION. ⚠️
3
4
  *
4
- * Debug page for testing OAuth refresh token flow.
5
- * Shows current session state, allows manual refresh trigger,
6
- * and can force-expire tokens for testing.
5
+ * Refresh Token Test Page. Shows current session state, allows a manual
6
+ * refresh trigger, and FORCE-EXPIRES the access token via a button that POSTs
7
+ * `/api/test/force-expire`. That endpoint MUTATES SESSION STATE.
7
8
  *
8
- * Usage:
9
+ * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000) ──────────────────────────
10
+ * The force-expire HANDLER was fixed (244877: default-deny behind
11
+ * ENABLE_TEST_ROUTES) and its docstring corrected to say so — but that fix
12
+ * covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
13
+ * public export carrying its own mount instructions, so a consumer can follow
14
+ * these lines and never once read the handler's warning.
15
+ *
16
+ * THIS PAGE HAS NO ENV GATE OF ITS OWN. Mounting it is the gate. The two are
17
+ * independent: mounting this page while ENABLE_TEST_ROUTES is unset gives you
18
+ * a button that 404s — confusing, but safe. The unsafe configuration is
19
+ * setting that flag anywhere this page is also reachable.
20
+ *
21
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
9
22
  * ```typescript
10
- * // app/test-env/refresh-token/page.tsx
23
+ * // app/test-env/refresh-token/page.tsx — TEST/DEV ENVIRONMENTS ONLY
11
24
  * export { RefreshTokenPage as default } from '@payez/next-mvp/pages/test-env';
12
25
  * ```
26
+ * Requires ENABLE_TEST_ROUTES=true IN THAT ENVIRONMENT ONLY for the button to
27
+ * do anything. Never set it in production. If you do not need to exercise the
28
+ * refresh flow, DO NOT MOUNT THIS AT ALL.
13
29
  */
14
30
  export declare function RefreshTokenPage(): import("react/jsx-runtime").JSX.Element;
15
31
  export default RefreshTokenPage;
@@ -6,17 +6,33 @@ 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
8
  /**
9
- * Refresh Token Test Page
9
+ * ⚠️ DEBUG PAGE THAT DRIVES A DESTRUCTIVE ENDPOINT — DO NOT MOUNT IN
10
+ * PRODUCTION. ⚠️
10
11
  *
11
- * Debug page for testing OAuth refresh token flow.
12
- * Shows current session state, allows manual refresh trigger,
13
- * and can force-expire tokens for testing.
12
+ * Refresh Token Test Page. Shows current session state, allows a manual
13
+ * refresh trigger, and FORCE-EXPIRES the access token via a button that POSTs
14
+ * `/api/test/force-expire`. That endpoint MUTATES SESSION STATE.
14
15
  *
15
- * Usage:
16
+ * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000) ──────────────────────────
17
+ * The force-expire HANDLER was fixed (244877: default-deny behind
18
+ * ENABLE_TEST_ROUTES) and its docstring corrected to say so — but that fix
19
+ * covered the ROUTE AND NOT THE PAGE THAT DRIVES IT. This page is a separate
20
+ * public export carrying its own mount instructions, so a consumer can follow
21
+ * these lines and never once read the handler's warning.
22
+ *
23
+ * THIS PAGE HAS NO ENV GATE OF ITS OWN. Mounting it is the gate. The two are
24
+ * independent: mounting this page while ENABLE_TEST_ROUTES is unset gives you
25
+ * a button that 404s — confusing, but safe. The unsafe configuration is
26
+ * setting that flag anywhere this page is also reachable.
27
+ *
28
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
16
29
  * ```typescript
17
- * // app/test-env/refresh-token/page.tsx
30
+ * // app/test-env/refresh-token/page.tsx — TEST/DEV ENVIRONMENTS ONLY
18
31
  * export { RefreshTokenPage as default } from '@payez/next-mvp/pages/test-env';
19
32
  * ```
33
+ * Requires ENABLE_TEST_ROUTES=true IN THAT ENVIRONMENT ONLY for the button to
34
+ * do anything. Never set it in production. If you do not need to exercise the
35
+ * refresh flow, DO NOT MOUNT THIS AT ALL.
20
36
  */
21
37
  function RefreshTokenPage() {
22
38
  const { data: session } = better_auth_client_1.authClient.useSession();
@@ -1,11 +1,23 @@
1
1
  /**
2
- * Test Environment Index Page
2
+ * ⚠️ DEBUG INDEX — DO NOT MOUNT IN PRODUCTION. ⚠️
3
3
  *
4
- * Debug tools index showing session status and links to debug pages.
4
+ * Test Environment Index Page. Shows session status and links to the other
5
+ * debug pages.
5
6
  *
6
- * Usage in consuming app:
7
+ * ────────────────────────── WHY THE INDEX IS ITS OWN FINDING (QAPert 35000 sweep) ──────────────────────────
8
+ * This page is DISCOVERY. On its own it only reads session status, so in
9
+ * isolation it looks like the harmless member of the set — but it
10
+ * ADVERTISES the sibling routes to anyone who reaches it, including the JWT
11
+ * inspector that puts a live session token on the clipboard. Mounting this
12
+ * turns four URLs somebody would otherwise have to guess into one linked
13
+ * index.
14
+ *
15
+ * If you mount the /test-env segment at all, gate the WHOLE SEGMENT in your
16
+ * middleware, per environment — not page by page.
17
+ *
18
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
7
19
  * ```typescript
8
- * // app/test-env/page.tsx
20
+ * // app/test-env/page.tsx — TEST/DEV ENVIRONMENTS ONLY
9
21
  * export { TestEnvPage as default } from '@payez/next-mvp/pages/test-env';
10
22
  * ```
11
23
  */
@@ -10,13 +10,25 @@ const better_auth_client_1 = require("../../client/better-auth-client");
10
10
  const react_1 = require("react");
11
11
  const link_1 = __importDefault(require("next/link"));
12
12
  /**
13
- * Test Environment Index Page
13
+ * ⚠️ DEBUG INDEX — DO NOT MOUNT IN PRODUCTION. ⚠️
14
14
  *
15
- * Debug tools index showing session status and links to debug pages.
15
+ * Test Environment Index Page. Shows session status and links to the other
16
+ * debug pages.
16
17
  *
17
- * Usage in consuming app:
18
+ * ────────────────────────── WHY THE INDEX IS ITS OWN FINDING (QAPert 35000 sweep) ──────────────────────────
19
+ * This page is DISCOVERY. On its own it only reads session status, so in
20
+ * isolation it looks like the harmless member of the set — but it
21
+ * ADVERTISES the sibling routes to anyone who reaches it, including the JWT
22
+ * inspector that puts a live session token on the clipboard. Mounting this
23
+ * turns four URLs somebody would otherwise have to guess into one linked
24
+ * index.
25
+ *
26
+ * If you mount the /test-env segment at all, gate the WHOLE SEGMENT in your
27
+ * middleware, per environment — not page by page.
28
+ *
29
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
18
30
  * ```typescript
19
- * // app/test-env/page.tsx
31
+ * // app/test-env/page.tsx — TEST/DEV ENVIRONMENTS ONLY
20
32
  * export { TestEnvPage as default } from '@payez/next-mvp/pages/test-env';
21
33
  * ```
22
34
  */
@@ -0,0 +1,4 @@
1
+ export declare function TestEnvPage(): import("react/jsx-runtime").JSX.Element;
2
+ export declare function JwtInspectPage(): import("react/jsx-runtime").JSX.Element;
3
+ export declare function RefreshTokenPage(): import("react/jsx-runtime").JSX.Element;
4
+ export declare function EmergencyLogoutPage(): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TestEnvPage = TestEnvPage;
4
+ exports.JwtInspectPage = JwtInspectPage;
5
+ exports.RefreshTokenPage = RefreshTokenPage;
6
+ exports.EmergencyLogoutPage = EmergencyLogoutPage;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ /**
9
+ * SERVER-COMPONENT GUARDS for the test-env debug pages (card 249259, BAPert
10
+ * ruling 35321). These are what `@payez/next-mvp/pages/test-env` exports. The
11
+ * raw client components are NOT exported.
12
+ *
13
+ * ── WHAT THIS IS, AND WHAT IT IS EMPHATICALLY NOT ───────────────────────────
14
+ * MOUNTING IS STILL THE PRIMARY GATE. This wrapper is a backstop for a mount
15
+ * that should not have happened. It is NOT a licence to mount these freely:
16
+ * setting ENABLE_TEST_ROUTES=true in an environment where these pages are
17
+ * mounted gives you exactly the exposure this package spent 248991 documenting
18
+ * - a live session token and a copy button. If you are not actively debugging,
19
+ * DO NOT MOUNT THESE AT ALL. The safest configuration remains their absence.
20
+ *
21
+ * ── WHY A SERVER COMPONENT, AND WHY THE PAGES THEMSELVES CANNOT DO THIS ─────
22
+ * 248991 item 2 asked for a per-page env gate and it was correctly REFUSED:
23
+ * the pages are `'use client'`, and non-`NEXT_PUBLIC_` variables are not
24
+ * available in the browser (Next.js docs, environment-variables: "Non-
25
+ * NEXT_PUBLIC_ environment variables are only available in the Node.js
26
+ * environment"). A check written inside those files would read `undefined` on
27
+ * the client and deny ALWAYS - killing the tool in dev too - while reading the
28
+ * REAL value during SSR, producing a hydration mismatch. It would look like a
29
+ * gate, satisfy a grep for one, and protect nothing.
30
+ *
31
+ * A SERVER component can read the variable for real. That is the whole reason
32
+ * this file exists as a separate layer instead of a few lines in each page.
33
+ *
34
+ * ── FAIL CLOSED ─────────────────────────────────────────────────────────────
35
+ * The test is `=== 'true'`, not truthiness. Unset, empty, "false", "0", "TRUE"
36
+ * and every typo all deny. The UNSAFE state requires a deliberate, exact act;
37
+ * the SAFE state is what you get by doing nothing, including by misconfiguring
38
+ * it. A gate whose default is "open" is not a gate.
39
+ *
40
+ * `notFound()` is deliberate and is not a 403: a real 404 is indistinguishable
41
+ * from a route that was never registered, whereas a 403 CONFIRMS the page
42
+ * exists to anyone who probes for it. That is the same disclosure axis card
43
+ * 245920 was raised about, so shipping the confirming variant here would
44
+ * reintroduce next door the thing being fixed there.
45
+ *
46
+ * notFound() throws (NEXT_HTTP_ERROR_FALLBACK;404) and terminates rendering of
47
+ * the segment, so nothing below it runs and the client component is never sent
48
+ * to the browser. It does not need `return` - it is typed `never`.
49
+ */
50
+ const navigation_1 = require("next/navigation");
51
+ const TestEnvPage_1 = require("./TestEnvPage");
52
+ const JwtInspectPage_1 = require("./JwtInspectPage");
53
+ const RefreshTokenPage_1 = require("./RefreshTokenPage");
54
+ const EmergencyLogoutPage_1 = require("./EmergencyLogoutPage");
55
+ /**
56
+ * The single gate. Exact-match on purpose - see FAIL CLOSED above.
57
+ *
58
+ * Read at REQUEST time inside each page rather than once at module scope: a
59
+ * module-scope constant would be captured at build time, so a consumer who
60
+ * changed the variable would keep whatever value was baked into the bundle and
61
+ * would have no way to tell. Reading per render costs an object property
62
+ * lookup and removes an entire class of "I turned it off and it stayed on".
63
+ */
64
+ function testRoutesEnabled() {
65
+ return process.env.ENABLE_TEST_ROUTES === 'true';
66
+ }
67
+ function TestEnvPage() {
68
+ if (!testRoutesEnabled())
69
+ (0, navigation_1.notFound)();
70
+ return (0, jsx_runtime_1.jsx)(TestEnvPage_1.TestEnvPage, {});
71
+ }
72
+ function JwtInspectPage() {
73
+ if (!testRoutesEnabled())
74
+ (0, navigation_1.notFound)();
75
+ return (0, jsx_runtime_1.jsx)(JwtInspectPage_1.JwtInspectPage, {});
76
+ }
77
+ function RefreshTokenPage() {
78
+ if (!testRoutesEnabled())
79
+ (0, navigation_1.notFound)();
80
+ return (0, jsx_runtime_1.jsx)(RefreshTokenPage_1.RefreshTokenPage, {});
81
+ }
82
+ function EmergencyLogoutPage() {
83
+ if (!testRoutesEnabled())
84
+ (0, navigation_1.notFound)();
85
+ return (0, jsx_runtime_1.jsx)(EmergencyLogoutPage_1.EmergencyLogoutPage, {});
86
+ }
@@ -1,9 +1,55 @@
1
1
  /**
2
- * Test Environment Pages
2
+ * ⚠️ TEST-ENVIRONMENT DEBUG PAGES — NONE OF THESE BELONG IN PRODUCTION. ⚠️
3
3
  *
4
- * Export debug pages for MVP consumers.
4
+ * ────────────────────────── READ BEFORE COPYING ANYTHING BELOW (QAPert 35000) ──────────────────────────
5
+ * This barrel is the SPREAD MECHANISM. It ships compiled into dist and into
6
+ * the .d.ts, so it reaches every consuming developer's editor as hover text,
7
+ * and the block below is a four-line copy-paste that mounts every debug page
8
+ * this package has. It previously carried no warning at all.
5
9
  *
6
- * Usage:
10
+ * NONE of these pages has an env gate of its own. Unlike the force-expire
11
+ * handler (ENABLE_TEST_ROUTES) or the idp-token route
12
+ * (ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as your
13
+ * middleware allows. MOUNTING IS THE GATE — so gate the WHOLE /test-env
14
+ * segment, per environment, not page by page.
15
+ *
16
+ * They are not equally dangerous. In descending order:
17
+ * JwtInspectPage WORST. Copy-to-clipboard for the LIVE SESSION TOKEN
18
+ * and ACCESS TOKEN. A session token is exactly the
19
+ * precondition the 246007 finding needs.
20
+ * TestEnvPage The index. Harmless alone, but it ADVERTISES the
21
+ * other three to anyone who reaches it.
22
+ * RefreshTokenPage Drives the destructive /api/test/force-expire.
23
+ * EmergencyLogoutPage Mildest. Self-inflicted, recoverable.
24
+ *
25
+ * ── THESE PAGES ARE NOW ENV-GATED, FAIL-CLOSED (249259, BAPert ruling 35321) ──
26
+ * SUPERSEDES the previous note here, which said a gate was impossible and that
27
+ * mounting was the only control. That was correct about the PAGES and wrong as
28
+ * a conclusion: it CANNOT be done inside these `'use client'` files, but it CAN
29
+ * be done in a server component wrapping them. That wrapper now ships, and it
30
+ * is what the four names below resolve to — see ./guarded.tsx.
31
+ *
32
+ * ENABLE_TEST_ROUTES === 'true' -> the page renders
33
+ * anything else, including unset -> notFound()
34
+ *
35
+ * It is the DEFAULT, not an opt-in sibling: BAPert's ruling was explicit that
36
+ * "an opt-in safety gate is one nobody opts into". The raw ungated components
37
+ * are NOT exported. Same import path, same component names, same signatures —
38
+ * consumers add one env var to their dev environments and change no code.
39
+ *
40
+ * ── MOUNTING IS STILL THE PRIMARY GATE ──────────────────────────────────────
41
+ * Do not read the env var as making mounting safe. The wrapper is a backstop
42
+ * for a mount that should not have happened. With ENABLE_TEST_ROUTES=true in an
43
+ * environment where these are mounted you have exactly the exposure 248991
44
+ * documented: a live session token, one click from the clipboard. If you are
45
+ * not actively debugging, DO NOT MOUNT THESE AT ALL — the safest configuration
46
+ * is still their absence, and the env var does not change that.
47
+ *
48
+ * Each page's own docstring carries the detail. If you are not actively
49
+ * debugging, DO NOT MOUNT THESE AT ALL — the safest configuration is their
50
+ * absence.
51
+ *
52
+ * Usage — TEST/DEV ENVIRONMENTS ONLY:
7
53
  * ```typescript
8
54
  * // app/test-env/page.tsx
9
55
  * export { TestEnvPage as default } from '@payez/next-mvp/pages/test-env';
@@ -18,7 +64,4 @@
18
64
  * export { EmergencyLogoutPage as default } from '@payez/next-mvp/pages/test-env';
19
65
  * ```
20
66
  */
21
- export { TestEnvPage, TestEnvPage as default } from './TestEnvPage';
22
- export { JwtInspectPage } from './JwtInspectPage';
23
- export { RefreshTokenPage } from './RefreshTokenPage';
24
- export { EmergencyLogoutPage } from './EmergencyLogoutPage';
67
+ export { TestEnvPage, TestEnvPage as default, JwtInspectPage, RefreshTokenPage, EmergencyLogoutPage, } from './guarded';