@payez/next-mvp 4.1.21 → 4.4.1

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 +162 -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
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSingleFlight = createSingleFlight;
4
+ function createSingleFlight(factory) {
5
+ let pending = null;
6
+ return {
7
+ get() {
8
+ if (pending)
9
+ return pending;
10
+ const attempt = factory();
11
+ // Cleanup runs on a SIDE branch so the promise handed to callers is the
12
+ // original one and still rejects. The identity guard means only the
13
+ // attempt that actually failed clears itself: without it, a slow failure
14
+ // could evict a newer attempt a later caller had already installed, and
15
+ // that caller would be de-duplicated against a promise nobody owns.
16
+ attempt.catch(() => {
17
+ if (pending === attempt)
18
+ pending = null;
19
+ });
20
+ pending = attempt;
21
+ return pending;
22
+ },
23
+ isPending() {
24
+ return pending !== null;
25
+ },
26
+ reset() {
27
+ pending = null;
28
+ },
29
+ };
30
+ }
@@ -278,6 +278,10 @@ async function ensureFreshToken(request) {
278
278
  * `mfaVerified || ... || false`).
279
279
  */
280
280
  mfaVerified: idpTokens?.mfaVerified ?? false,
281
+ // R807 (362550): the fail-closed default above stays exactly as documented;
282
+ // this records whether it was a measurement or a default. `!== undefined`
283
+ // deliberately - an explicit `false` is a signal.
284
+ mfaSignalPresent: idpTokens?.mfaVerified !== undefined,
281
285
  oauthProvider: idpTokens?.oauthProvider ?? baSession.oauthProvider ?? undefined,
282
286
  idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId ?? baSession.idpClientId,
283
287
  merchantId: idpTokens?.merchantId ?? baSession.merchantId,
@@ -304,6 +308,14 @@ async function ensureFreshToken(request) {
304
308
  * UNSET is the only honest provider. Both are fail-closed.
305
309
  */
306
310
  mfaVerified: false,
311
+ /**
312
+ * R807 (362550) - FIFTH construction site, not in the ruling's list of four.
313
+ * This path has NO IDP TOKENS AT ALL, so there is no producer statement to
314
+ * record: the `false` above is a DEFAULT, never a measurement. `false` is
315
+ * therefore the honest signal value, and it is the conservative one for
316
+ * viability (absent/false -> do not force a re-challenge).
317
+ */
318
+ mfaSignalPresent: false,
307
319
  oauthProvider: undefined,
308
320
  };
309
321
  }
@@ -187,6 +187,18 @@ function makeAuthDecision(ctx) {
187
187
  if (sessionPointer.hasRefreshToken) {
188
188
  return { type: 'refresh_needed' };
189
189
  }
190
+ // 307290: this is the TERMINAL arm of the expired branch - refresh is not
191
+ // possible here, so the only outcomes are "redirect" or "allow". Every other
192
+ // terminal branch in this function (:242 no_session, :259 force_invalidated,
193
+ // :271 stale_session, :318 2FA-incomplete) already exempts /api/ so the route
194
+ // can answer with a real 401 JSON; this one did not, and redirecting an API
195
+ // request to an HTML login page makes the failure INVISIBLE - fetch() follows
196
+ // the 307, gets 200 HTML, resolves successfully and parses garbage.
197
+ // MUST stay BELOW the hasRefreshToken check above: placed any higher it would
198
+ // intercept 'refresh_needed' and disable middleware token refresh for the
199
+ // entire API surface.
200
+ if (pathname.startsWith('/api/'))
201
+ return { type: 'allow' }; // Let API return proper 401 JSON
190
202
  return {
191
203
  type: 'redirect',
192
204
  location: `${LOGIN_PAGE}?callbackUrl=${encodeURIComponent(safeCallback)}&error=SessionExpired`,
@@ -48,6 +48,45 @@ export interface SessionData {
48
48
  bearerKeyId?: string;
49
49
  /** Whether MFA has been verified for this session */
50
50
  mfaVerified: boolean;
51
+ /**
52
+ * *** R807 (BAPert, card 362550). DID THE PRODUCER ACTUALLY SAY ANYTHING ABOUT MFA? ***
53
+ *
54
+ * `mfaVerified` above is deliberately FAIL-CLOSED: every construction site
55
+ * collapses an absent value to `false` (`?? false`), so by the time anyone
56
+ * reads it, "the producer said false" and "the producer said nothing" are
57
+ * indistinguishable. That collapse is CORRECT and must not be removed - a
58
+ * security consumer treating absence as "verified" is an AUTH BYPASS.
59
+ *
60
+ * But `routes/auth/viability.ts` asks a genuinely different question: "did the
61
+ * OAuth callback AFFIRMATIVELY set false, or did we simply never know?" It
62
+ * cannot ask that of a field whose absence has already been collapsed. This
63
+ * field carries that one bit, so the collapse can stay.
64
+ *
65
+ * true -> the producer emitted an explicit mfaVerified; `mfaVerified` is a measurement.
66
+ * false -> the producer emitted nothing; `mfaVerified`'s `false` is a DEFAULT, not a signal.
67
+ * undefined -> LEGACY SESSION, minted before this field existed. Same meaning as `false`
68
+ * for viability (do NOT force a re-challenge), and it is why this is
69
+ * optional rather than defaulted: `?? false` here would be the very
70
+ * absence-destroying move this field exists to undo.
71
+ *
72
+ * *** CONFINED, BY RULING: THIS FIELD MUST NEVER REACH AN AUTH DECISION. ***
73
+ * It exists to make viability LESS aggressive (suppressing a mass re-challenge of
74
+ * the pre-existing session population). Letting it influence an auth path would
75
+ * turn a permissive read into a fail-open, and R807 is void if it does. Gate on
76
+ * `mfaVerified` for anything that grants access; gate on this only to decide
77
+ * whether `mfaVerified === false` was a real statement.
78
+ *
79
+ * *** DECLARING IT HERE IS LOAD-BEARING FOR MORE THAN DOCUMENTATION. ***
80
+ * `SessionData` carries `[key: string]: any` (the "backward compatibility"
81
+ * member at the END of this interface, added for migration back-compat — it
82
+ * was line 147 before this block was inserted above it, which is the number
83
+ * quoted in R807/362550; adding these lines moved it down, so find it by name,
84
+ * not by number). Any field NOT declared explicitly is absorbed by that index
85
+ * signature and becomes `any` - so an undeclared `mfaSignalPresent` would let a
86
+ * typo at the consumer compile clean and read `undefined` forever, silently
87
+ * disabling the viability override. Measured, not assumed: see the card.
88
+ */
89
+ mfaSignalPresent?: boolean;
51
90
  /**
52
91
  * The IDP's own routing decision for a 2FA-pending login (cards 141688 /
53
92
  * 141741, IDP WO 113023). Wire name is snake — `next_step`.
@@ -111,6 +150,8 @@ export declare class SessionModel {
111
150
  decodedAccessToken?: any;
112
151
  bearerKeyId?: string;
113
152
  mfaVerified: boolean;
153
+ /** R807: see SessionData.mfaSignalPresent. Propagated verbatim, never defaulted. */
154
+ mfaSignalPresent?: boolean;
114
155
  mfaMethod?: 'email' | 'sms' | 'totp';
115
156
  mfaCompletedAt?: number;
116
157
  mfaExpiresAt?: number;
@@ -40,6 +40,8 @@ class SessionModel {
40
40
  bearerKeyId;
41
41
  // MFA State
42
42
  mfaVerified;
43
+ /** R807: see SessionData.mfaSignalPresent. Propagated verbatim, never defaulted. */
44
+ mfaSignalPresent;
43
45
  mfaMethod;
44
46
  mfaCompletedAt;
45
47
  mfaExpiresAt;
@@ -69,6 +71,10 @@ class SessionModel {
69
71
  this.bearerKeyId = data.bearerKeyId;
70
72
  // MFA State
71
73
  this.mfaVerified = data.mfaVerified ?? false;
74
+ // *** R807: PROPAGATE, DO NOT DEFAULT. *** `?? false` here would destroy the
75
+ // legacy/never-knew distinction on every path through this class - invisibly,
76
+ // because callers construct a SessionModel without restating the field.
77
+ this.mfaSignalPresent = data.mfaSignalPresent;
72
78
  this.mfaMethod = data.mfaMethod;
73
79
  this.mfaCompletedAt = data.mfaCompletedAt;
74
80
  this.mfaExpiresAt = data.mfaExpiresAt;
@@ -122,6 +128,9 @@ class SessionModel {
122
128
  decodedAccessToken: this.decodedAccessToken,
123
129
  bearerKeyId: this.bearerKeyId,
124
130
  mfaVerified: this.mfaVerified,
131
+ // R807: must survive the Redis round-trip, or the session is re-read as
132
+ // `legacy` one hop later and viability silently stops firing.
133
+ mfaSignalPresent: this.mfaSignalPresent,
125
134
  mfaMethod: this.mfaMethod,
126
135
  mfaCompletedAt: this.mfaCompletedAt,
127
136
  mfaExpiresAt: this.mfaExpiresAt,
@@ -24,7 +24,7 @@ interface AdminLoginFormProps {
24
24
  /** Optional logo component to render */
25
25
  logo?: React.ReactNode;
26
26
  }
27
- declare function AdminLoginForm({ title, subtitle, callbackUrl: propCallbackUrl, logo, }: AdminLoginFormProps): import("react/jsx-runtime").JSX.Element;
27
+ declare function AdminLoginForm({ title, subtitle, logo, }: AdminLoginFormProps): import("react/jsx-runtime").JSX.Element;
28
28
  declare function AdminLoginFallback(): import("react/jsx-runtime").JSX.Element;
29
29
  export default function AdminLoginPage(props: AdminLoginFormProps): import("react/jsx-runtime").JSX.Element;
30
30
  export { AdminLoginForm, AdminLoginFallback };
@@ -21,53 +21,17 @@ exports.AdminLoginForm = AdminLoginForm;
21
21
  exports.AdminLoginFallback = AdminLoginFallback;
22
22
  const jsx_runtime_1 = require("react/jsx-runtime");
23
23
  const react_1 = require("react");
24
- const better_auth_client_1 = require("../../client/better-auth-client");
25
- const navigation_1 = require("next/navigation");
26
- const react_2 = require("react");
27
24
  const useTheme_1 = require("../../theme/useTheme");
28
- function AdminLoginForm({ title = 'Admin Login', subtitle = 'Authorized personnel only', callbackUrl: propCallbackUrl, logo, }) {
29
- const searchParams = (0, navigation_1.useSearchParams)();
30
- const callbackUrl = propCallbackUrl || searchParams?.get('callbackUrl') || '/dashboard';
25
+ function AdminLoginForm({ title = 'Admin Login', subtitle = 'Authorized personnel only', logo, }) {
31
26
  const branding = (0, useTheme_1.useBranding)();
32
27
  const colors = (0, useTheme_1.useColors)();
33
- const [email, setEmail] = (0, react_1.useState)('');
34
- const [password, setPassword] = (0, react_1.useState)('');
35
- const [showPassword, setShowPassword] = (0, react_1.useState)(false);
36
- const [isLoading, setIsLoading] = (0, react_1.useState)(false);
37
- const [error, setError] = (0, react_1.useState)(null);
38
- const handleSubmit = async (e) => {
39
- e.preventDefault();
40
- setIsLoading(true);
41
- setError(null);
42
- try {
43
- const result = await better_auth_client_1.authClient.signIn.email({
44
- email,
45
- password,
46
- callbackURL: callbackUrl,
47
- });
48
- if (result?.error) {
49
- const errorMsg = typeof result.error === 'object'
50
- ? result.error.message || 'Invalid credentials'
51
- : String(result.error);
52
- setError(errorMsg);
53
- }
54
- else if (result?.data) {
55
- // Redirect to verify-code for 2FA or directly to callback
56
- window.location.href = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
57
- }
58
- }
59
- catch (err) {
60
- setError('An unexpected error occurred');
61
- }
62
- finally {
63
- setIsLoading(false);
64
- }
65
- };
66
- return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full", children: [logo && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: logo })), (0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold text-center mb-2 text-slate-900", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-center mb-8 text-slate-600", children: subtitle }), error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6 px-4 py-3 rounded-lg bg-red-500 text-white text-center text-sm", children: error })), (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-email", className: "block text-sm font-medium mb-2 text-slate-700", children: "Email" }), (0, jsx_runtime_1.jsx)("input", { id: "admin-email", type: "email", value: email, onChange: (e) => setEmail(e.target.value), required: true, disabled: isLoading, autoComplete: "email", className: "w-full px-4 py-3 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "admin@example.com" })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { htmlFor: "admin-password", className: "block text-sm font-medium mb-2 text-slate-700", children: "Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { id: "admin-password", type: showPassword ? 'text' : 'password', value: password, onChange: (e) => setPassword(e.target.value), required: true, disabled: isLoading, autoComplete: "current-password", className: "w-full px-4 py-3 pr-12 rounded-lg border border-slate-300 bg-white text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed", placeholder: "Enter your password" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPassword(!showPassword), className: "absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700", "aria-label": showPassword ? 'Hide password' : 'Show password', children: showPassword ? ((0, jsx_runtime_1.jsx)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.878 9.878L6.464 6.464m7.535 7.535l3.415 3.414M3 3l3.464 3.464M21 21l-3.415-3.414" }) })) : ((0, jsx_runtime_1.jsxs)("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", className: "w-5 h-5", children: [(0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z" }), (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" })] })) })] })] }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: isLoading, className: "w-full py-3 px-4 rounded-lg font-semibold text-white transition-colors bg-slate-700 hover:bg-slate-800 disabled:bg-slate-400 disabled:cursor-not-allowed", children: isLoading ? ((0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin -ml-1 mr-3 h-5 w-5 text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), "Signing in..."] })) : ('Sign In') })] }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-6 text-center text-xs text-slate-500", children: ["This login is for authorized administrators only.", (0, jsx_runtime_1.jsx)("br", {}), "All access attempts are logged."] })] }) }));
28
+ void branding;
29
+ void colors;
30
+ return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900 p-4", children: (0, jsx_runtime_1.jsxs)("div", { className: "bg-white rounded-2xl shadow-2xl p-8 max-w-md w-full", children: [logo && ((0, jsx_runtime_1.jsx)("div", { className: "flex justify-center mb-6", children: logo })), (0, jsx_runtime_1.jsx)("h1", { className: "text-2xl font-bold text-center mb-2 text-slate-900", children: title }), (0, jsx_runtime_1.jsx)("p", { className: "text-center mb-8 text-slate-600", children: subtitle }), (0, jsx_runtime_1.jsxs)("p", { className: "mt-6 text-center text-xs text-slate-500", children: ["This login is for authorized administrators only.", (0, jsx_runtime_1.jsx)("br", {}), "All access attempts are logged."] })] }) }));
67
31
  }
68
32
  function AdminLoginFallback() {
69
33
  return ((0, jsx_runtime_1.jsx)("div", { className: "min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-700 via-slate-800 to-slate-900", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsxs)("svg", { className: "animate-spin h-10 w-10 mx-auto text-white", fill: "none", viewBox: "0 0 24 24", children: [(0, jsx_runtime_1.jsx)("circle", { className: "opacity-25", cx: "12", cy: "12", r: "10", stroke: "currentColor", strokeWidth: "4" }), (0, jsx_runtime_1.jsx)("path", { className: "opacity-75", fill: "currentColor", d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" })] }), (0, jsx_runtime_1.jsx)("p", { className: "mt-4 text-slate-400", children: "Loading..." })] }) }));
70
34
  }
71
35
  function AdminLoginPage(props) {
72
- return ((0, jsx_runtime_1.jsx)(react_2.Suspense, { fallback: (0, jsx_runtime_1.jsx)(AdminLoginFallback, {}), children: (0, jsx_runtime_1.jsx)(AdminLoginForm, { ...props }) }));
36
+ return ((0, jsx_runtime_1.jsx)(react_1.Suspense, { fallback: (0, jsx_runtime_1.jsx)(AdminLoginFallback, {}), children: (0, jsx_runtime_1.jsx)(AdminLoginForm, { ...props }) }));
73
37
  }
@@ -10,5 +10,39 @@
10
10
  * - Danger zone (data export, account deletion)
11
11
  *
12
12
  * @see docs/specs/MEMBER_SELF_SERVICE_SPEC.md
13
+ *
14
+ * @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). The canonical,
15
+ * maintained copy is graft-local:
16
+ * idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
17
+ * What stage serves is what was scored against Clerk; this package copy was
18
+ * imported by nothing in the graft and receives NO further fixes. Do not
19
+ * import it into new work.
20
+ */
21
+ export interface EnhancedSecurityPageProps {
22
+ /**
23
+ * 152745. Where "Enable 2FA" navigates to start enrollment. Deliberately a
24
+ * PROP, not a hardcoded route: this component ships in a shared package
25
+ * consumed by apps whose enrollment routes differ (idp_admin_ui_graft has
26
+ * one at /user-authentication/multi-factor; idealresume — per QAPert's
27
+ * 170047 gate — has NONE at all). Hardcoding either app's route into the
28
+ * package would be wrong for the other, and would silently reproduce the
29
+ * exact "advertises 2FA, nothing reachable" defect this card exists to
30
+ * fix. Undefined -> the button renders honestly disabled (see below),
31
+ * never a fake-live control pointing nowhere.
32
+ */
33
+ mfaEnrollHref?: string;
34
+ /**
35
+ * 152745. Called when "Disable 2FA" is clicked. Left to the consumer for
36
+ * the same reason as mfaEnrollHref: disabling 2FA is a real, consequential
37
+ * write (DELETE /api/Account/mfa/methods/{id} per cluster card 170059) and
38
+ * this package has no opinion on which method id, confirmation flow, or
39
+ * step-up policy a given app requires. Undefined -> honestly disabled.
40
+ */
41
+ onDisable2FA?: () => Promise<void> | void;
42
+ }
43
+ /**
44
+ * @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). Canonical copy:
45
+ * idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
46
+ * This copy receives NO further fixes. Do not import.
13
47
  */
14
- export default function EnhancedSecurityPage(): import("react/jsx-runtime").JSX.Element;
48
+ export default function EnhancedSecurityPage({ mfaEnrollHref, onDisable2FA }?: EnhancedSecurityPageProps): import("react/jsx-runtime").JSX.Element;
@@ -11,6 +11,13 @@
11
11
  * - Danger zone (data export, account deletion)
12
12
  *
13
13
  * @see docs/specs/MEMBER_SELF_SERVICE_SPEC.md
14
+ *
15
+ * @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). The canonical,
16
+ * maintained copy is graft-local:
17
+ * idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
18
+ * What stage serves is what was scored against Clerk; this package copy was
19
+ * imported by nothing in the graft and receives NO further fixes. Do not
20
+ * import it into new work.
14
21
  */
15
22
  'use client';
16
23
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -147,10 +154,32 @@ function formatRelativeTime(dateString) {
147
154
  return dateString;
148
155
  }
149
156
  }
150
- function EnhancedSecurityPage() {
157
+ /**
158
+ * @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). Canonical copy:
159
+ * idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
160
+ * This copy receives NO further fixes. Do not import.
161
+ */
162
+ function EnhancedSecurityPage({ mfaEnrollHref, onDisable2FA } = {}) {
151
163
  const { data: profileData, isLoading: isProfileLoading } = (0, useProfile_1.useProfile)();
152
164
  const layout = (0, useTheme_1.useLayout)();
153
165
  const colors = (0, useTheme_1.useColors)();
166
+ const [disabling2FA, setDisabling2FA] = (0, react_1.useState)(false);
167
+ const [disable2FAError, setDisable2FAError] = (0, react_1.useState)('');
168
+ const handleDisable2FA = async () => {
169
+ if (!onDisable2FA || disabling2FA)
170
+ return;
171
+ setDisable2FAError('');
172
+ try {
173
+ setDisabling2FA(true);
174
+ await onDisable2FA();
175
+ }
176
+ catch (err) {
177
+ setDisable2FAError(err?.message || 'Failed to disable two-factor authentication');
178
+ }
179
+ finally {
180
+ setDisabling2FA(false);
181
+ }
182
+ };
154
183
  // Security data state (would come from API in production)
155
184
  const [securityData, setSecurityData] = (0, react_1.useState)(null);
156
185
  const [loadingSecurityData, setLoadingSecurityData] = (0, react_1.useState)(true);
@@ -244,5 +273,16 @@ function EnhancedSecurityPage() {
244
273
  }
245
274
  const twoFactorEnabled = profileData?.two_factor_enabled || securityData?.two_factor?.enabled || false;
246
275
  const twoFactorMethod = securityData?.two_factor?.method_display || (twoFactorEnabled ? 'SMS' : undefined);
247
- return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen ${bgClass}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `${layout?.maxWidth || 'max-w-3xl'} mx-auto ${layout?.padding || 'p-6'} space-y-6`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: `text-2xl font-bold ${textPrimary}`, children: "Security" }), (0, jsx_runtime_1.jsx)("p", { className: `mt-1 ${textMuted}`, children: "Manage your account security settings" })] }), (0, jsx_runtime_1.jsxs)("div", { className: `rounded-lg border ${cardBg} ${borderColor} p-6`, children: [(0, jsx_runtime_1.jsx)("h2", { className: `text-lg font-semibold mb-4 ${textPrimary}`, children: "Security Summary" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "2FA Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: twoFactorEnabled, isDark: isDark }), twoFactorMethod && (0, jsx_runtime_1.jsxs)("p", { className: `text-xs mt-2 ${textMuted}`, children: ["Method: ", twoFactorMethod] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "Email Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: profileData?.email_confirmed || false, label: profileData?.email_confirmed ? 'Verified' : 'Not Verified', isDark: isDark }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-2 ${textMuted} truncate`, children: profileData?.email })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "Phone Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: profileData?.phone_confirmed || false, label: profileData?.phone_confirmed ? 'Verified' : 'Not Verified', isDark: isDark }), profileData?.phone_number && (0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-2 ${textMuted}`, children: profileData.phone_number })] })] })] }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Password", description: securityData?.password?.last_changed ? `Last changed ${formatDate(securityData.password.last_changed)}` : undefined, isDark: isDark, children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handlePasswordSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "Current Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.current ? 'text' : 'password', value: formData.current_password, onChange: e => setFormData({ ...formData, current_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Enter current password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, current: !p.current })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.current ? 'Hide' : 'Show' })] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.new ? 'text' : 'password', value: formData.new_password, onChange: e => setFormData({ ...formData, new_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Enter new password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, new: !p.new })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.new ? 'Hide' : 'Show' })] }), formData.new_password && ((0, jsx_runtime_1.jsx)("div", { className: "mt-2", children: (0, jsx_runtime_1.jsx)(PasswordStrengthMeter, { score: newPasswordScore, failedRequirements: failedRequirements, tip: tip, isDark: isDark }) })), policy && formData.new_password && ((0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: (0, jsx_runtime_1.jsx)(PolicyChecklist, { policy: policy, password: formData.new_password, isDark: isDark }) }))] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "Confirm New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.confirm ? 'text' : 'password', value: formData.confirm_password, onChange: e => setFormData({ ...formData, confirm_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Confirm new password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, confirm: !p.confirm })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.confirm ? 'Hide' : 'Show' })] }), formData.confirm_password && !passwordsMatch && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-red-400", children: "Passwords do not match" }), formData.confirm_password && passwordsMatch && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-green-400", children: "Passwords match" })] }), error && (0, jsx_runtime_1.jsx)("div", { className: "bg-red-900/30 border border-red-600 rounded-lg p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-red-400 text-sm", children: error }) }), success && (0, jsx_runtime_1.jsx)("div", { className: "bg-green-900/30 border border-green-600 rounded-lg p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-green-400 text-sm", children: success }) }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: !canSubmit, className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", children: submitting ? 'Changing...' : 'Change Password' })] }) }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Two-Factor Authentication", description: "Add an extra layer of security to your account", isDark: isDark, children: (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Status" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: twoFactorEnabled ? `Enabled via ${twoFactorMethod || 'SMS'}` : 'Not enabled' })] }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: twoFactorEnabled, isDark: isDark })] }), twoFactorEnabled ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { className: `px-3 py-1.5 text-sm rounded border ${isDark ? 'border-slate-600 hover:bg-slate-700' : 'border-gray-300 hover:bg-gray-50'} ${textSecondary}`, children: "Change Method" }), (0, jsx_runtime_1.jsx)("button", { className: "px-3 py-1.5 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10", children: "Disable 2FA" })] })) : ((0, jsx_runtime_1.jsx)("button", { className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700", children: "Enable 2FA" })), securityData?.two_factor?.backup_codes_remaining !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: `mt-4 p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textSecondary}`, children: "Backup Codes" }), (0, jsx_runtime_1.jsxs)("p", { className: textMuted, children: ["You have ", securityData.two_factor.backup_codes_remaining, " of 10 backup codes remaining"] }), (0, jsx_runtime_1.jsx)("button", { className: `mt-2 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "View Codes" })] }))] }) }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Connected Accounts", description: "OAuth providers linked to your account", isDark: isDark, children: [securityData?.connected_accounts && securityData.connected_accounts.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: securityData.connected_accounts.map((account, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: account.provider }), account.email && (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: account.email }), account.connected_at && (0, jsx_runtime_1.jsxs)("p", { className: `text-xs ${textMuted}`, children: ["Connected ", formatDate(account.connected_at)] })] }), account.can_disconnect && ((0, jsx_runtime_1.jsx)("button", { className: "text-sm text-red-500 hover:text-red-400", children: "Disconnect" }))] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No connected accounts" })), (0, jsx_runtime_1.jsx)("button", { className: `mt-4 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "Connect Another Account" })] }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Active Sessions", description: "Devices currently logged into your account", isDark: isDark, children: [securityData?.active_sessions && securityData.active_sessions.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: securityData.active_sessions.map((session, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: session.device_name || 'Unknown Device' }), session.is_current && (0, jsx_runtime_1.jsx)("span", { className: "px-2 py-0.5 rounded text-xs bg-blue-500 text-white", children: "This Device" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: session.location || session.ip_address }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: session.is_current ? 'Active now' : `Last active ${formatRelativeTime(session.last_active_at)}` })] }), !session.is_current && ((0, jsx_runtime_1.jsx)("button", { className: "text-sm text-red-500 hover:text-red-400", children: "Revoke" }))] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "Session information not available" })), (0, jsx_runtime_1.jsx)("button", { className: "mt-4 text-sm text-red-500 hover:text-red-400", children: "Sign Out All Other Devices" })] }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Recent Activity", description: "Security events on your account", isDark: isDark, children: [securityData?.recent_activity && securityData.recent_activity.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-2", children: securityData.recent_activity.slice(0, 5).map((activity, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between py-2 border-b last:border-b-0 ${borderColor}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { className: activity.success ? 'text-green-400' : 'text-red-400', children: activity.success ? 'v' : 'x' }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textPrimary}`, children: activity.description }), activity.device && (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: activity.device })] })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: formatRelativeTime(activity.timestamp) })] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No recent activity" })), (0, jsx_runtime_1.jsx)("button", { className: `mt-4 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "View Full History" })] }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Danger Zone", description: "Irreversible actions", isDark: isDark, children: (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-4 rounded-lg border border-yellow-500/50`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Download My Data" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: "Get a copy of all your personal data" })] }), (0, jsx_runtime_1.jsx)("button", { className: "px-4 py-2 text-sm rounded border border-yellow-500 text-yellow-500 hover:bg-yellow-500/10", children: "Request Export" })] }), (0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-4 rounded-lg border border-red-500/50`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Delete Account" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: "Permanently delete your account and all data" })] }), (0, jsx_runtime_1.jsx)("button", { className: "px-4 py-2 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10", children: "Delete Account" })] })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-center", children: (0, jsx_runtime_1.jsx)("a", { href: "/account/profile", className: `text-sm hover:underline ${textMuted}`, children: "Back to Profile" }) })] }) }));
276
+ return ((0, jsx_runtime_1.jsx)("div", { className: `min-h-screen ${bgClass}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `${layout?.maxWidth || 'max-w-3xl'} mx-auto ${layout?.padding || 'p-6'} space-y-6`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: `text-2xl font-bold ${textPrimary}`, children: "Security" }), (0, jsx_runtime_1.jsx)("p", { className: `mt-1 ${textMuted}`, children: "Manage your account security settings" })] }), (0, jsx_runtime_1.jsxs)("div", { className: `rounded-lg border ${cardBg} ${borderColor} p-6`, children: [(0, jsx_runtime_1.jsx)("h2", { className: `text-lg font-semibold mb-4 ${textPrimary}`, children: "Security Summary" }), (0, jsx_runtime_1.jsxs)("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "2FA Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: twoFactorEnabled, isDark: isDark }), twoFactorMethod && (0, jsx_runtime_1.jsxs)("p", { className: `text-xs mt-2 ${textMuted}`, children: ["Method: ", twoFactorMethod] })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "Email Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: profileData?.email_confirmed || false, label: profileData?.email_confirmed ? 'Verified' : 'Not Verified', isDark: isDark }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-2 ${textMuted} truncate`, children: profileData?.email })] }), (0, jsx_runtime_1.jsxs)("div", { className: `p-4 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm font-medium mb-2 ${textSecondary}`, children: "Phone Status" }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: profileData?.phone_confirmed || false, label: profileData?.phone_confirmed ? 'Verified' : 'Not Verified', isDark: isDark }), profileData?.phone_number && (0, jsx_runtime_1.jsx)("p", { className: `text-xs mt-2 ${textMuted}`, children: profileData.phone_number })] })] })] }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Password", description: securityData?.password?.last_changed ? `Last changed ${formatDate(securityData.password.last_changed)}` : undefined, isDark: isDark, children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handlePasswordSubmit, className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "Current Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.current ? 'text' : 'password', value: formData.current_password, onChange: e => setFormData({ ...formData, current_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Enter current password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, current: !p.current })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.current ? 'Hide' : 'Show' })] })] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.new ? 'text' : 'password', value: formData.new_password, onChange: e => setFormData({ ...formData, new_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Enter new password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, new: !p.new })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.new ? 'Hide' : 'Show' })] }), formData.new_password && ((0, jsx_runtime_1.jsx)("div", { className: "mt-2", children: (0, jsx_runtime_1.jsx)(PasswordStrengthMeter, { score: newPasswordScore, failedRequirements: failedRequirements, tip: tip, isDark: isDark }) })), policy && formData.new_password && ((0, jsx_runtime_1.jsx)("div", { className: "mt-3", children: (0, jsx_runtime_1.jsx)(PolicyChecklist, { policy: policy, password: formData.new_password, isDark: isDark }) }))] }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: `block text-sm font-medium mb-2 ${textSecondary}`, children: "Confirm New Password" }), (0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("input", { type: showPasswords.confirm ? 'text' : 'password', value: formData.confirm_password, onChange: e => setFormData({ ...formData, confirm_password: e.target.value }), className: `w-full px-3 py-2 rounded-md border focus:outline-none focus:ring-2 focus:ring-blue-500 ${inputBg} ${inputBorder} ${inputText}`, placeholder: "Confirm new password", disabled: submitting }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: () => setShowPasswords(p => ({ ...p, confirm: !p.confirm })), className: `absolute inset-y-0 right-2 flex items-center ${textMuted}`, children: showPasswords.confirm ? 'Hide' : 'Show' })] }), formData.confirm_password && !passwordsMatch && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-red-400", children: "Passwords do not match" }), formData.confirm_password && passwordsMatch && (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-green-400", children: "Passwords match" })] }), error && (0, jsx_runtime_1.jsx)("div", { className: "bg-red-900/30 border border-red-600 rounded-lg p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-red-400 text-sm", children: error }) }), success && (0, jsx_runtime_1.jsx)("div", { className: "bg-green-900/30 border border-green-600 rounded-lg p-3", children: (0, jsx_runtime_1.jsx)("p", { className: "text-green-400 text-sm", children: success }) }), (0, jsx_runtime_1.jsx)("button", { type: "submit", disabled: !canSubmit, className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", children: submitting ? 'Changing...' : 'Change Password' })] }) }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Two-Factor Authentication", description: "Add an extra layer of security to your account", isDark: isDark, children: (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Status" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: twoFactorEnabled ? `Enabled via ${twoFactorMethod || 'SMS'}` : 'Not enabled' })] }), (0, jsx_runtime_1.jsx)(StatusBadge, { enabled: twoFactorEnabled, isDark: isDark })] }), twoFactorEnabled ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col gap-2", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex gap-2", children: [(0, jsx_runtime_1.jsx)("button", { className: `px-3 py-1.5 text-sm rounded border ${isDark ? 'border-slate-600 hover:bg-slate-700' : 'border-gray-300 hover:bg-gray-50'} ${textSecondary}`, children: "Change Method" }), (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: handleDisable2FA, disabled: !onDisable2FA || disabling2FA, title: !onDisable2FA ? 'Disabling two-factor authentication is not available here' : undefined, className: "px-3 py-1.5 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10 disabled:opacity-40 disabled:cursor-not-allowed disabled:hover:bg-transparent", children: disabling2FA ? 'Disabling...' : 'Disable 2FA' })] }), disable2FAError && (0, jsx_runtime_1.jsx)("p", { className: "text-xs text-red-400", children: disable2FAError })] })) : mfaEnrollHref ? (
277
+ // 152745: real navigation to the consumer's own enrollment
278
+ // route (a plain <a>, matching "Back to Profile" below — this
279
+ // package has no opinion on which router the host app uses).
280
+ (0, jsx_runtime_1.jsx)("a", { href: mfaEnrollHref, className: "inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700", children: "Enable 2FA" })) : (
281
+ // 152745: no enrollment destination configured. This is the
282
+ // case QAPert's 170047 gate found live on idealresume.online —
283
+ // rendering an active-looking button here would be the exact
284
+ // "advertises 2FA, nothing reachable" defect this card exists
285
+ // to close, just moved from "no onClick" to "onClick navigates
286
+ // to a 404". Honestly disabled instead.
287
+ (0, jsx_runtime_1.jsx)("button", { type: "button", disabled: true, title: "Two-factor authentication enrollment is not available here", className: "px-4 py-2 bg-blue-600 text-white rounded-md opacity-40 cursor-not-allowed", children: "Enable 2FA" })), securityData?.two_factor?.backup_codes_remaining !== undefined && ((0, jsx_runtime_1.jsxs)("div", { className: `mt-4 p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textSecondary}`, children: "Backup Codes" }), (0, jsx_runtime_1.jsxs)("p", { className: textMuted, children: ["You have ", securityData.two_factor.backup_codes_remaining, " of 10 backup codes remaining"] }), (0, jsx_runtime_1.jsx)("button", { className: `mt-2 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "View Codes" })] }))] }) }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Connected Accounts", description: "OAuth providers linked to your account", isDark: isDark, children: [securityData?.connected_accounts && securityData.connected_accounts.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: securityData.connected_accounts.map((account, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: account.provider }), account.email && (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: account.email }), account.connected_at && (0, jsx_runtime_1.jsxs)("p", { className: `text-xs ${textMuted}`, children: ["Connected ", formatDate(account.connected_at)] })] }), account.can_disconnect && ((0, jsx_runtime_1.jsx)("button", { className: "text-sm text-red-500 hover:text-red-400", children: "Disconnect" }))] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No connected accounts" })), (0, jsx_runtime_1.jsx)("button", { className: `mt-4 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "Connect Another Account" })] }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Active Sessions", description: "Devices currently logged into your account", isDark: isDark, children: [securityData?.active_sessions && securityData.active_sessions.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-3", children: securityData.active_sessions.map((session, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-3 rounded-lg ${elevatedBg}`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: session.device_name || 'Unknown Device' }), session.is_current && (0, jsx_runtime_1.jsx)("span", { className: "px-2 py-0.5 rounded text-xs bg-blue-500 text-white", children: "This Device" })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: session.location || session.ip_address }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: session.is_current ? 'Active now' : `Last active ${formatRelativeTime(session.last_active_at)}` })] }), !session.is_current && ((0, jsx_runtime_1.jsx)("button", { className: "text-sm text-red-500 hover:text-red-400", children: "Revoke" }))] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "Session information not available" })), (0, jsx_runtime_1.jsx)("button", { className: "mt-4 text-sm text-red-500 hover:text-red-400", children: "Sign Out All Other Devices" })] }), (0, jsx_runtime_1.jsxs)(SecuritySection, { title: "Recent Activity", description: "Security events on your account", isDark: isDark, children: [securityData?.recent_activity && securityData.recent_activity.length > 0 ? ((0, jsx_runtime_1.jsx)("div", { className: "space-y-2", children: securityData.recent_activity.slice(0, 5).map((activity, idx) => ((0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between py-2 border-b last:border-b-0 ${borderColor}`, children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)("span", { className: activity.success ? 'text-green-400' : 'text-red-400', children: activity.success ? 'v' : 'x' }), (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textPrimary}`, children: activity.description }), activity.device && (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: activity.device })] })] }), (0, jsx_runtime_1.jsx)("p", { className: `text-xs ${textMuted}`, children: formatRelativeTime(activity.timestamp) })] }, idx))) })) : ((0, jsx_runtime_1.jsx)("p", { className: textMuted, children: "No recent activity" })), (0, jsx_runtime_1.jsx)("button", { className: `mt-4 text-sm ${isDark ? 'text-blue-400' : 'text-blue-600'}`, children: "View Full History" })] }), (0, jsx_runtime_1.jsx)(SecuritySection, { title: "Danger Zone", description: "Irreversible actions", isDark: isDark, children: (0, jsx_runtime_1.jsxs)("div", { className: "space-y-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-4 rounded-lg border border-yellow-500/50`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Download My Data" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: "Get a copy of all your personal data" })] }), (0, jsx_runtime_1.jsx)("button", { className: "px-4 py-2 text-sm rounded border border-yellow-500 text-yellow-500 hover:bg-yellow-500/10", children: "Request Export" })] }), (0, jsx_runtime_1.jsxs)("div", { className: `flex items-center justify-between p-4 rounded-lg border border-red-500/50`, children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("p", { className: `font-medium ${textPrimary}`, children: "Delete Account" }), (0, jsx_runtime_1.jsx)("p", { className: `text-sm ${textMuted}`, children: "Permanently delete your account and all data" })] }), (0, jsx_runtime_1.jsx)("button", { className: "px-4 py-2 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10", children: "Delete Account" })] })] }) }), (0, jsx_runtime_1.jsx)("div", { className: "text-center", children: (0, jsx_runtime_1.jsx)("a", { href: "/account/profile", className: `text-sm hover:underline ${textMuted}`, children: "Back to Profile" }) })] }) }));
248
288
  }
@@ -5,4 +5,5 @@
5
5
  * - EnhancedSecurityPage: Full-featured security with password, 2FA, sessions, activity, danger zone
6
6
  */
7
7
  export { default as SecurityPage } from './page';
8
+ /** @deprecated use the graft-local canonical copy (card 221074) */
8
9
  export { default as EnhancedSecurityPage } from './EnhancedSecurityPage';
@@ -12,5 +12,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.EnhancedSecurityPage = exports.SecurityPage = void 0;
13
13
  var page_1 = require("./page");
14
14
  Object.defineProperty(exports, "SecurityPage", { enumerable: true, get: function () { return __importDefault(page_1).default; } });
15
+ // RETIRED (BAPert ruling, card 221074, 2026-08-18): canonical copy is
16
+ // graft-local at idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
17
+ /** @deprecated use the graft-local canonical copy (card 221074) */
15
18
  var EnhancedSecurityPage_1 = require("./EnhancedSecurityPage");
16
19
  Object.defineProperty(exports, "EnhancedSecurityPage", { enumerable: true, get: function () { return __importDefault(EnhancedSecurityPage_1).default; } });
@@ -1,12 +1,24 @@
1
1
  /**
2
- * Emergency Logout Page
2
+ * ⚠️ DESTRUCTIVE DEBUG PAGE — NOT INTENDED FOR PRODUCTION. ⚠️
3
3
  *
4
- * Nuclear option for clearing auth state when things go wrong.
5
- * Clears cookies, localStorage, and forces NextAuth signout.
4
+ * Emergency Logout Page. Nuclear option for clearing auth state when things go
5
+ * wrong: clears cookies, clears localStorage, and forces a signout.
6
6
  *
7
- * Usage in consuming app:
7
+ * ────────────────────────── SCOPE, STATED HONESTLY (QAPert 35000 sweep) ──────────────────────────
8
+ * This is the MILDEST of the four test-env pages and this warning is
9
+ * deliberately weaker than its siblings'. The damage is SELF-INFLICTED and
10
+ * recoverable: a visitor can only destroy their OWN client-side auth state,
11
+ * and the remedy is to sign in again. It exposes no credential and mutates no
12
+ * other user's state. Do not read the banner above as equivalent to the JWT
13
+ * inspector's — inflating a mild finding is how real ones get discounted.
14
+ *
15
+ * It is flagged only because it ships with no env gate of its own, and because
16
+ * clearing localStorage wholesale can discard unrelated application state that
17
+ * a consuming app kept there. Mounting it is the gate.
18
+ *
19
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
8
20
  * ```typescript
9
- * // app/test-env/emergency-logout/page.tsx
21
+ * // app/test-env/emergency-logout/page.tsx — TEST/DEV ENVIRONMENTS ONLY
10
22
  * export { EmergencyLogoutPage as default } from '@payez/next-mvp/pages/test-env';
11
23
  * ```
12
24
  */
@@ -6,14 +6,26 @@ const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  const better_auth_client_1 = require("../../client/better-auth-client");
7
7
  const react_1 = require("react");
8
8
  /**
9
- * Emergency Logout Page
9
+ * ⚠️ DESTRUCTIVE DEBUG PAGE — NOT INTENDED FOR PRODUCTION. ⚠️
10
10
  *
11
- * Nuclear option for clearing auth state when things go wrong.
12
- * Clears cookies, localStorage, and forces NextAuth signout.
11
+ * Emergency Logout Page. Nuclear option for clearing auth state when things go
12
+ * wrong: clears cookies, clears localStorage, and forces a signout.
13
13
  *
14
- * Usage in consuming app:
14
+ * ────────────────────────── SCOPE, STATED HONESTLY (QAPert 35000 sweep) ──────────────────────────
15
+ * This is the MILDEST of the four test-env pages and this warning is
16
+ * deliberately weaker than its siblings'. The damage is SELF-INFLICTED and
17
+ * recoverable: a visitor can only destroy their OWN client-side auth state,
18
+ * and the remedy is to sign in again. It exposes no credential and mutates no
19
+ * other user's state. Do not read the banner above as equivalent to the JWT
20
+ * inspector's — inflating a mild finding is how real ones get discounted.
21
+ *
22
+ * It is flagged only because it ships with no env gate of its own, and because
23
+ * clearing localStorage wholesale can discard unrelated application state that
24
+ * a consuming app kept there. Mounting it is the gate.
25
+ *
26
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
15
27
  * ```typescript
16
- * // app/test-env/emergency-logout/page.tsx
28
+ * // app/test-env/emergency-logout/page.tsx — TEST/DEV ENVIRONMENTS ONLY
17
29
  * export { EmergencyLogoutPage as default } from '@payez/next-mvp/pages/test-env';
18
30
  * ```
19
31
  */
@@ -1,14 +1,39 @@
1
1
  /**
2
- * Session Inspector Page
2
+ * ⚠️ DEBUG PAGE THAT DISPLAYS AND COPIES LIVE CREDENTIALS — DO NOT MOUNT
3
+ * IN PRODUCTION. ⚠️
3
4
  *
4
- * Debug page for inspecting session data from Redis.
5
- * Shows user info, roles, 2FA status, and tokens.
5
+ * Session Inspector Page. Debug page for inspecting session data from Redis:
6
+ * shows user info, roles, 2FA status, and TOKENS. It offers
7
+ * COPY-TO-CLIPBOARD buttons for the live SESSION TOKEN and ACCESS TOKEN, and
8
+ * reads them from the consuming app's `/api/internal/session-data` route.
6
9
  *
7
- * Usage in consuming app:
10
+ * ────────────────────────── WHY THIS WARNING EXISTS (QAPert 35000 / 248991) ──────────────────────────
11
+ * THIS PAGE HAS NO ENV GATE OF ITS OWN. Unlike the force-expire handler
12
+ * (default-denied behind ENABLE_TEST_ROUTES) or the idp-token route (behind
13
+ * ENABLE_TOKEN_INSPECTOR), a mounted page is reachable exactly as far as the
14
+ * consuming app's own middleware allows. MOUNTING IT IS THE GATE. There is no
15
+ * second line of defence behind it.
16
+ *
17
+ * AND THE CREDENTIAL IT HANDS OVER IS THE PRECONDITION FOR A REAL ATTACK ON
18
+ * THIS SAME PACKAGE. The 246007 finding reduces to "a caller who KNOWS A
19
+ * SESSION TOKEN can act on a session it does not own". The purpose of this
20
+ * page is to put a live session token on the clipboard of whoever loads it. A
21
+ * screen-share, a shoulder-surfed screenshot or a support call is enough.
22
+ * Treat the rendered value as a live credential, because that is what it is.
23
+ *
24
+ * NOTE FOR MAINTAINERS: this warning must stay on THIS docstring, the one
25
+ * attached to the exported declaration. An identical block placed higher in
26
+ * the file does NOT reach the emitted .d.ts — measured, after I made exactly
27
+ * that mistake — so it would warn no consumer at all.
28
+ *
29
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
8
30
  * ```typescript
9
- * // app/test-env/jwt-inspect/page.tsx
31
+ * // app/test-env/jwt-inspect/page.tsx — TEST/DEV ENVIRONMENTS ONLY
10
32
  * export { JwtInspectPage as default } from '@payez/next-mvp/pages/test-env';
11
33
  * ```
34
+ * Gate the whole /test-env segment in your middleware, per environment. If you
35
+ * are not actively debugging token contents, DO NOT MOUNT THIS AT ALL — the
36
+ * safest configuration is the absence of the route.
12
37
  */
13
38
  export declare function JwtInspectPage(): import("react/jsx-runtime").JSX.Element;
14
39
  export default JwtInspectPage;