@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.
- package/POST-INSTALL.md +408 -0
- package/README.md +7 -0
- package/dist/api-handlers/account/recovery/initiate.js +35 -2
- package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/send-code.js +35 -2
- package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
- package/dist/api-handlers/account/recovery/verify-code.js +35 -2
- package/dist/api-handlers/account/reset-password.js +35 -2
- package/dist/api-handlers/account/validate-password.js +21 -3
- package/dist/api-handlers/admin/analytics.js +2 -1
- package/dist/api-handlers/admin/audit.js +2 -1
- package/dist/api-handlers/admin/sessions.js +2 -1
- package/dist/api-handlers/admin/stats.js +2 -1
- package/dist/api-handlers/admin/users.js +2 -1
- package/dist/api-handlers/admin/vibe-data.js +2 -1
- package/dist/api-handlers/auth/jwks.js +10 -0
- package/dist/api-handlers/auth/login.js +5 -2
- package/dist/api-handlers/auth/passkey-signin.js +16 -1
- package/dist/api-handlers/auth/refresh.d.ts +23 -3
- package/dist/api-handlers/auth/refresh.js +107 -16
- package/dist/api-handlers/auth/signout.js +3 -2
- package/dist/api-handlers/auth/verify-code.js +5 -2
- package/dist/api-handlers/session/viability.js +27 -1
- package/dist/api-handlers/test/force-expire.d.ts +29 -15
- package/dist/api-handlers/test/force-expire.js +109 -14
- package/dist/auth/better-auth.d.ts +263 -6
- package/dist/auth/better-auth.js +437 -59
- package/dist/auth/utils/idp-client.js +1 -1
- package/dist/client/AuthContext.js +49 -17
- package/dist/client/better-auth-client.d.ts +62 -62
- package/dist/components/account/UserAvatarMenu.js +12 -0
- package/dist/components/auth/ModeAwareLoginPage.js +10 -8
- package/dist/components/auth/ModeAwareSignupPage.js +5 -2
- package/dist/components/auth/TraditionalAuthSection.js +5 -1
- package/dist/hooks/useAuthSettings.js +14 -2
- package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
- package/dist/hooks/usePublicAuthSettings.js +28 -14
- package/dist/lib/client-key.d.ts +61 -0
- package/dist/lib/client-key.js +72 -0
- package/dist/lib/demo-mode.js +2 -1
- package/dist/lib/ensure-fresh-access-token.js +5 -1
- package/dist/lib/idp-client-config.d.ts +25 -2
- package/dist/lib/idp-client-config.js +381 -36
- package/dist/lib/idp-fetch.d.ts +2 -0
- package/dist/lib/idp-fetch.js +31 -1
- package/dist/lib/idp-url-guard.d.ts +29 -0
- package/dist/lib/idp-url-guard.js +38 -0
- package/dist/lib/redis.js +77 -5
- package/dist/lib/session-cookie-shape.d.ts +69 -0
- package/dist/lib/session-cookie-shape.js +126 -0
- package/dist/lib/session-store.d.ts +35 -2
- package/dist/lib/session-store.js +135 -4
- package/dist/lib/single-flight.d.ts +35 -0
- package/dist/lib/single-flight.js +30 -0
- package/dist/lib/token-lifecycle.js +12 -0
- package/dist/middleware/create-middleware.js +12 -0
- package/dist/models/SessionModel.d.ts +41 -0
- package/dist/models/SessionModel.js +9 -0
- package/dist/pages/admin-login/page.d.ts +1 -1
- package/dist/pages/admin-login/page.js +5 -41
- package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
- package/dist/pages/security/EnhancedSecurityPage.js +42 -2
- package/dist/pages/security/index.d.ts +1 -0
- package/dist/pages/security/index.js +3 -0
- package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
- package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
- package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
- package/dist/pages/test-env/JwtInspectPage.js +166 -7
- package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
- package/dist/pages/test-env/RefreshTokenPage.js +22 -6
- package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
- package/dist/pages/test-env/TestEnvPage.js +16 -4
- package/dist/pages/test-env/guarded.d.ts +4 -0
- package/dist/pages/test-env/guarded.js +86 -0
- package/dist/pages/test-env/index.d.ts +50 -7
- package/dist/pages/test-env/index.js +58 -12
- package/dist/routes/auth/logout.js +3 -2
- package/dist/routes/auth/refresh.js +27 -1
- package/dist/routes/auth/session.d.ts +12 -3
- package/dist/routes/auth/session.js +92 -9
- package/dist/routes/auth/settings.d.ts +34 -4
- package/dist/routes/auth/settings.js +87 -33
- package/dist/routes/auth/viability.d.ts +1 -1
- package/dist/routes/auth/viability.js +122 -11
- package/dist/server/auth-guard.js +22 -15
- package/dist/server/auth.d.ts +177 -20
- package/dist/server/auth.js +34 -75
- package/dist/server/decode-session.js +84 -10
- package/dist/types/auth.d.ts +18 -0
- package/package.json +969 -958
- package/scripts/check-internal-url-usage.sh +73 -73
- package/scripts/check-publish-health.mjs +120 -0
- package/scripts/dev-broker.ps1 +71 -6
- package/scripts/print-client-config.mjs +162 -0
- package/scripts/server-only-stub.cjs +4 -0
- package/src/api-handlers/account/recovery/initiate.ts +35 -2
- package/src/api-handlers/account/recovery/send-code.ts +36 -2
- package/src/api-handlers/account/recovery/verify-code.ts +36 -2
- package/src/api-handlers/account/reset-password.ts +35 -2
- package/src/api-handlers/account/validate-password.ts +22 -3
- package/src/api-handlers/admin/analytics.ts +2 -1
- package/src/api-handlers/admin/audit.ts +2 -1
- package/src/api-handlers/admin/sessions.ts +2 -1
- package/src/api-handlers/admin/stats.ts +2 -1
- package/src/api-handlers/admin/users.ts +2 -1
- package/src/api-handlers/admin/vibe-data.ts +2 -1
- package/src/api-handlers/auth/jwks.ts +10 -0
- package/src/api-handlers/auth/login.ts +5 -2
- package/src/api-handlers/auth/passkey-signin.ts +112 -98
- package/src/api-handlers/auth/refresh.ts +112 -15
- package/src/api-handlers/auth/signout.ts +4 -3
- package/src/api-handlers/auth/verify-code.ts +5 -2
- package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
- package/src/api-handlers/mfa/backup-codes.ts +41 -41
- package/src/api-handlers/mfa/methods.ts +46 -46
- package/src/api-handlers/mfa/totp-confirm.ts +42 -42
- package/src/api-handlers/mfa/totp-enroll.ts +42 -42
- package/src/api-handlers/passkeys/list.ts +31 -31
- package/src/api-handlers/passkeys/manage.ts +54 -54
- package/src/api-handlers/passkeys/register-options.ts +42 -42
- package/src/api-handlers/passkeys/register-verify.ts +41 -41
- package/src/api-handlers/session/viability.ts +29 -1
- package/src/api-handlers/test/force-expire.ts +118 -13
- package/src/auth/better-auth.ts +486 -51
- package/src/auth/utils/idp-client.ts +1 -1
- package/src/client/AuthContext.tsx +55 -19
- package/src/components/account/UserAvatarMenu.tsx +11 -0
- package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
- package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
- package/src/components/auth/TraditionalAuthSection.tsx +5 -1
- package/src/hooks/useAuthSettings.ts +14 -2
- package/src/hooks/usePublicAuthSettings.ts +38 -16
- package/src/lib/client-key.ts +88 -0
- package/src/lib/demo-mode.ts +2 -1
- package/src/lib/ensure-fresh-access-token.ts +5 -1
- package/src/lib/idp-client-config.ts +399 -36
- package/src/lib/idp-fetch.ts +36 -1
- package/src/lib/idp-url-guard.ts +37 -0
- package/src/lib/redis.ts +84 -6
- package/src/lib/session-cookie-shape.ts +184 -0
- package/src/lib/session-store.ts +152 -6
- package/src/lib/single-flight.ts +64 -0
- package/src/lib/token-lifecycle.ts +12 -0
- package/src/middleware/create-middleware.ts +11 -0
- package/src/models/SessionModel.ts +49 -0
- package/src/pages/admin-login/page.tsx +17 -127
- package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
- package/src/pages/security/index.ts +3 -0
- package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
- package/src/pages/test-env/JwtInspectPage.tsx +235 -20
- package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
- package/src/pages/test-env/TestEnvPage.tsx +16 -4
- package/src/pages/test-env/guarded.tsx +81 -0
- package/src/pages/test-env/index.ts +59 -7
- package/src/routes/auth/logout.ts +4 -3
- package/src/routes/auth/passkey-signin.ts +13 -13
- package/src/routes/auth/refresh.ts +30 -1
- package/src/routes/auth/session.ts +98 -9
- package/src/routes/auth/settings.ts +144 -42
- package/src/routes/auth/viability.ts +129 -11
- package/src/routes/mfa/backup-code-verify.ts +1 -1
- package/src/routes/mfa/backup-codes.ts +10 -10
- package/src/routes/mfa/methods.ts +22 -22
- package/src/routes/mfa/totp-confirm.ts +10 -10
- package/src/routes/mfa/totp-enroll.ts +10 -10
- package/src/routes/mfa/totp-verify.ts +14 -14
- package/src/routes/passkeys/list.ts +10 -10
- package/src/routes/passkeys/manage.ts +23 -23
- package/src/routes/passkeys/register-options.ts +10 -10
- package/src/routes/passkeys/register-verify.ts +10 -10
- package/src/server/auth-guard.ts +24 -17
- package/src/server/auth.ts +37 -73
- package/src/server/decode-session.ts +95 -11
- package/src/templates/instrumentation.ts +21 -2
- package/src/types/auth.ts +20 -0
- package/dist/lib/auth-secret.d.ts +0 -17
- package/dist/lib/auth-secret.js +0 -116
- package/src/lib/auth-secret.ts +0 -137
|
@@ -34,16 +34,41 @@ function decodeJwtPayload(token) {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
2
|
+
* ⚠️ DEBUG PAGE THAT DRIVES A DESTRUCTIVE ENDPOINT — DO NOT MOUNT IN
|
|
3
|
+
* PRODUCTION. ⚠️
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
9
|
+
* ⚠️ DEBUG PAGE THAT DRIVES A DESTRUCTIVE ENDPOINT — DO NOT MOUNT IN
|
|
10
|
+
* PRODUCTION. ⚠️
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
2
|
+
* ⚠️ DEBUG INDEX — DO NOT MOUNT IN PRODUCTION. ⚠️
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Test Environment Index Page. Shows session status and links to the other
|
|
5
|
+
* debug pages.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
13
|
+
* ⚠️ DEBUG INDEX — DO NOT MOUNT IN PRODUCTION. ⚠️
|
|
14
14
|
*
|
|
15
|
-
*
|
|
15
|
+
* Test Environment Index Page. Shows session status and links to the other
|
|
16
|
+
* debug pages.
|
|
16
17
|
*
|
|
17
|
-
*
|
|
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
|
-
*
|
|
2
|
+
* ⚠️ TEST-ENVIRONMENT DEBUG PAGES — NONE OF THESE BELONG IN PRODUCTION. ⚠️
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
*
|
|
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 './
|
|
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';
|