@payez/next-mvp 4.1.21 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/POST-INSTALL.md +408 -0
  2. package/README.md +7 -0
  3. package/dist/api-handlers/account/recovery/initiate.js +35 -2
  4. package/dist/api-handlers/account/recovery/send-code.d.ts +0 -33
  5. package/dist/api-handlers/account/recovery/send-code.js +35 -2
  6. package/dist/api-handlers/account/recovery/verify-code.d.ts +0 -33
  7. package/dist/api-handlers/account/recovery/verify-code.js +35 -2
  8. package/dist/api-handlers/account/reset-password.js +35 -2
  9. package/dist/api-handlers/account/validate-password.js +21 -3
  10. package/dist/api-handlers/admin/analytics.js +2 -1
  11. package/dist/api-handlers/admin/audit.js +2 -1
  12. package/dist/api-handlers/admin/sessions.js +2 -1
  13. package/dist/api-handlers/admin/stats.js +2 -1
  14. package/dist/api-handlers/admin/users.js +2 -1
  15. package/dist/api-handlers/admin/vibe-data.js +2 -1
  16. package/dist/api-handlers/auth/jwks.js +10 -0
  17. package/dist/api-handlers/auth/login.js +5 -2
  18. package/dist/api-handlers/auth/passkey-signin.js +16 -1
  19. package/dist/api-handlers/auth/refresh.d.ts +23 -3
  20. package/dist/api-handlers/auth/refresh.js +107 -16
  21. package/dist/api-handlers/auth/signout.js +3 -2
  22. package/dist/api-handlers/auth/verify-code.js +5 -2
  23. package/dist/api-handlers/session/viability.js +27 -1
  24. package/dist/api-handlers/test/force-expire.d.ts +29 -15
  25. package/dist/api-handlers/test/force-expire.js +109 -14
  26. package/dist/auth/better-auth.d.ts +263 -6
  27. package/dist/auth/better-auth.js +437 -59
  28. package/dist/auth/utils/idp-client.js +1 -1
  29. package/dist/client/AuthContext.js +49 -17
  30. package/dist/client/better-auth-client.d.ts +62 -62
  31. package/dist/components/account/UserAvatarMenu.js +12 -0
  32. package/dist/components/auth/ModeAwareLoginPage.js +10 -8
  33. package/dist/components/auth/ModeAwareSignupPage.js +5 -2
  34. package/dist/components/auth/TraditionalAuthSection.js +5 -1
  35. package/dist/hooks/useAuthSettings.js +14 -2
  36. package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
  37. package/dist/hooks/usePublicAuthSettings.js +28 -14
  38. package/dist/lib/client-key.d.ts +61 -0
  39. package/dist/lib/client-key.js +72 -0
  40. package/dist/lib/demo-mode.js +2 -1
  41. package/dist/lib/ensure-fresh-access-token.js +5 -1
  42. package/dist/lib/idp-client-config.d.ts +25 -2
  43. package/dist/lib/idp-client-config.js +381 -36
  44. package/dist/lib/idp-fetch.d.ts +2 -0
  45. package/dist/lib/idp-fetch.js +31 -1
  46. package/dist/lib/idp-url-guard.d.ts +29 -0
  47. package/dist/lib/idp-url-guard.js +38 -0
  48. package/dist/lib/redis.js +77 -5
  49. package/dist/lib/session-cookie-shape.d.ts +69 -0
  50. package/dist/lib/session-cookie-shape.js +126 -0
  51. package/dist/lib/session-store.d.ts +35 -2
  52. package/dist/lib/session-store.js +135 -4
  53. package/dist/lib/single-flight.d.ts +35 -0
  54. package/dist/lib/single-flight.js +30 -0
  55. package/dist/lib/token-lifecycle.js +12 -0
  56. package/dist/middleware/create-middleware.js +12 -0
  57. package/dist/models/SessionModel.d.ts +41 -0
  58. package/dist/models/SessionModel.js +9 -0
  59. package/dist/pages/admin-login/page.d.ts +1 -1
  60. package/dist/pages/admin-login/page.js +5 -41
  61. package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
  62. package/dist/pages/security/EnhancedSecurityPage.js +42 -2
  63. package/dist/pages/security/index.d.ts +1 -0
  64. package/dist/pages/security/index.js +3 -0
  65. package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
  66. package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
  67. package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
  68. package/dist/pages/test-env/JwtInspectPage.js +166 -7
  69. package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
  70. package/dist/pages/test-env/RefreshTokenPage.js +22 -6
  71. package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
  72. package/dist/pages/test-env/TestEnvPage.js +16 -4
  73. package/dist/pages/test-env/guarded.d.ts +4 -0
  74. package/dist/pages/test-env/guarded.js +86 -0
  75. package/dist/pages/test-env/index.d.ts +50 -7
  76. package/dist/pages/test-env/index.js +58 -12
  77. package/dist/routes/auth/logout.js +3 -2
  78. package/dist/routes/auth/refresh.js +27 -1
  79. package/dist/routes/auth/session.d.ts +12 -3
  80. package/dist/routes/auth/session.js +92 -9
  81. package/dist/routes/auth/settings.d.ts +34 -4
  82. package/dist/routes/auth/settings.js +87 -33
  83. package/dist/routes/auth/viability.d.ts +1 -1
  84. package/dist/routes/auth/viability.js +122 -11
  85. package/dist/server/auth-guard.js +22 -15
  86. package/dist/server/auth.d.ts +177 -20
  87. package/dist/server/auth.js +34 -75
  88. package/dist/server/decode-session.js +84 -10
  89. package/dist/types/auth.d.ts +18 -0
  90. package/package.json +969 -958
  91. package/scripts/check-internal-url-usage.sh +73 -73
  92. package/scripts/check-publish-health.mjs +120 -0
  93. package/scripts/dev-broker.ps1 +71 -6
  94. package/scripts/print-client-config.mjs +162 -0
  95. package/scripts/server-only-stub.cjs +4 -0
  96. package/src/api-handlers/account/recovery/initiate.ts +35 -2
  97. package/src/api-handlers/account/recovery/send-code.ts +36 -2
  98. package/src/api-handlers/account/recovery/verify-code.ts +36 -2
  99. package/src/api-handlers/account/reset-password.ts +35 -2
  100. package/src/api-handlers/account/validate-password.ts +22 -3
  101. package/src/api-handlers/admin/analytics.ts +2 -1
  102. package/src/api-handlers/admin/audit.ts +2 -1
  103. package/src/api-handlers/admin/sessions.ts +2 -1
  104. package/src/api-handlers/admin/stats.ts +2 -1
  105. package/src/api-handlers/admin/users.ts +2 -1
  106. package/src/api-handlers/admin/vibe-data.ts +2 -1
  107. package/src/api-handlers/auth/jwks.ts +10 -0
  108. package/src/api-handlers/auth/login.ts +5 -2
  109. package/src/api-handlers/auth/passkey-signin.ts +112 -98
  110. package/src/api-handlers/auth/refresh.ts +112 -15
  111. package/src/api-handlers/auth/signout.ts +4 -3
  112. package/src/api-handlers/auth/verify-code.ts +5 -2
  113. package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
  114. package/src/api-handlers/mfa/backup-codes.ts +41 -41
  115. package/src/api-handlers/mfa/methods.ts +46 -46
  116. package/src/api-handlers/mfa/totp-confirm.ts +42 -42
  117. package/src/api-handlers/mfa/totp-enroll.ts +42 -42
  118. package/src/api-handlers/passkeys/list.ts +31 -31
  119. package/src/api-handlers/passkeys/manage.ts +54 -54
  120. package/src/api-handlers/passkeys/register-options.ts +42 -42
  121. package/src/api-handlers/passkeys/register-verify.ts +41 -41
  122. package/src/api-handlers/session/viability.ts +29 -1
  123. package/src/api-handlers/test/force-expire.ts +118 -13
  124. package/src/auth/better-auth.ts +486 -51
  125. package/src/auth/utils/idp-client.ts +1 -1
  126. package/src/client/AuthContext.tsx +55 -19
  127. package/src/components/account/UserAvatarMenu.tsx +11 -0
  128. package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
  129. package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
  130. package/src/components/auth/TraditionalAuthSection.tsx +5 -1
  131. package/src/hooks/useAuthSettings.ts +14 -2
  132. package/src/hooks/usePublicAuthSettings.ts +38 -16
  133. package/src/lib/client-key.ts +88 -0
  134. package/src/lib/demo-mode.ts +2 -1
  135. package/src/lib/ensure-fresh-access-token.ts +5 -1
  136. package/src/lib/idp-client-config.ts +399 -36
  137. package/src/lib/idp-fetch.ts +36 -1
  138. package/src/lib/idp-url-guard.ts +37 -0
  139. package/src/lib/redis.ts +84 -6
  140. package/src/lib/session-cookie-shape.ts +184 -0
  141. package/src/lib/session-store.ts +152 -6
  142. package/src/lib/single-flight.ts +64 -0
  143. package/src/lib/token-lifecycle.ts +12 -0
  144. package/src/middleware/create-middleware.ts +11 -0
  145. package/src/models/SessionModel.ts +49 -0
  146. package/src/pages/admin-login/page.tsx +17 -127
  147. package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
  148. package/src/pages/security/index.ts +3 -0
  149. package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
  150. package/src/pages/test-env/JwtInspectPage.tsx +235 -20
  151. package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
  152. package/src/pages/test-env/TestEnvPage.tsx +16 -4
  153. package/src/pages/test-env/guarded.tsx +81 -0
  154. package/src/pages/test-env/index.ts +59 -7
  155. package/src/routes/auth/logout.ts +4 -3
  156. package/src/routes/auth/passkey-signin.ts +13 -13
  157. package/src/routes/auth/refresh.ts +30 -1
  158. package/src/routes/auth/session.ts +98 -9
  159. package/src/routes/auth/settings.ts +144 -42
  160. package/src/routes/auth/viability.ts +129 -11
  161. package/src/routes/mfa/backup-code-verify.ts +1 -1
  162. package/src/routes/mfa/backup-codes.ts +10 -10
  163. package/src/routes/mfa/methods.ts +22 -22
  164. package/src/routes/mfa/totp-confirm.ts +10 -10
  165. package/src/routes/mfa/totp-enroll.ts +10 -10
  166. package/src/routes/mfa/totp-verify.ts +14 -14
  167. package/src/routes/passkeys/list.ts +10 -10
  168. package/src/routes/passkeys/manage.ts +23 -23
  169. package/src/routes/passkeys/register-options.ts +10 -10
  170. package/src/routes/passkeys/register-verify.ts +10 -10
  171. package/src/server/auth-guard.ts +24 -17
  172. package/src/server/auth.ts +37 -73
  173. package/src/server/decode-session.ts +95 -11
  174. package/src/templates/instrumentation.ts +21 -2
  175. package/src/types/auth.ts +20 -0
  176. package/dist/lib/auth-secret.d.ts +0 -17
  177. package/dist/lib/auth-secret.js +0 -116
  178. package/src/lib/auth-secret.ts +0 -137
@@ -77,6 +77,46 @@ export interface SessionData {
77
77
  /** Whether MFA has been verified for this session */
78
78
  mfaVerified: boolean;
79
79
 
80
+ /**
81
+ * *** R807 (BAPert, card 362550). DID THE PRODUCER ACTUALLY SAY ANYTHING ABOUT MFA? ***
82
+ *
83
+ * `mfaVerified` above is deliberately FAIL-CLOSED: every construction site
84
+ * collapses an absent value to `false` (`?? false`), so by the time anyone
85
+ * reads it, "the producer said false" and "the producer said nothing" are
86
+ * indistinguishable. That collapse is CORRECT and must not be removed - a
87
+ * security consumer treating absence as "verified" is an AUTH BYPASS.
88
+ *
89
+ * But `routes/auth/viability.ts` asks a genuinely different question: "did the
90
+ * OAuth callback AFFIRMATIVELY set false, or did we simply never know?" It
91
+ * cannot ask that of a field whose absence has already been collapsed. This
92
+ * field carries that one bit, so the collapse can stay.
93
+ *
94
+ * true -> the producer emitted an explicit mfaVerified; `mfaVerified` is a measurement.
95
+ * false -> the producer emitted nothing; `mfaVerified`'s `false` is a DEFAULT, not a signal.
96
+ * undefined -> LEGACY SESSION, minted before this field existed. Same meaning as `false`
97
+ * for viability (do NOT force a re-challenge), and it is why this is
98
+ * optional rather than defaulted: `?? false` here would be the very
99
+ * absence-destroying move this field exists to undo.
100
+ *
101
+ * *** CONFINED, BY RULING: THIS FIELD MUST NEVER REACH AN AUTH DECISION. ***
102
+ * It exists to make viability LESS aggressive (suppressing a mass re-challenge of
103
+ * the pre-existing session population). Letting it influence an auth path would
104
+ * turn a permissive read into a fail-open, and R807 is void if it does. Gate on
105
+ * `mfaVerified` for anything that grants access; gate on this only to decide
106
+ * whether `mfaVerified === false` was a real statement.
107
+ *
108
+ * *** DECLARING IT HERE IS LOAD-BEARING FOR MORE THAN DOCUMENTATION. ***
109
+ * `SessionData` carries `[key: string]: any` (the "backward compatibility"
110
+ * member at the END of this interface, added for migration back-compat — it
111
+ * was line 147 before this block was inserted above it, which is the number
112
+ * quoted in R807/362550; adding these lines moved it down, so find it by name,
113
+ * not by number). Any field NOT declared explicitly is absorbed by that index
114
+ * signature and becomes `any` - so an undeclared `mfaSignalPresent` would let a
115
+ * typo at the consumer compile clean and read `undefined` forever, silently
116
+ * disabling the viability override. Measured, not assumed: see the card.
117
+ */
118
+ mfaSignalPresent?: boolean;
119
+
80
120
  /**
81
121
  * The IDP's own routing decision for a 2FA-pending login (cards 141688 /
82
122
  * 141741, IDP WO 113023). Wire name is snake — `next_step`.
@@ -175,6 +215,8 @@ export class SessionModel {
175
215
 
176
216
  // MFA State
177
217
  mfaVerified: boolean;
218
+ /** R807: see SessionData.mfaSignalPresent. Propagated verbatim, never defaulted. */
219
+ mfaSignalPresent?: boolean;
178
220
  mfaMethod?: 'email' | 'sms' | 'totp';
179
221
  mfaCompletedAt?: number;
180
222
  mfaExpiresAt?: number;
@@ -209,6 +251,10 @@ export class SessionModel {
209
251
 
210
252
  // MFA State
211
253
  this.mfaVerified = data.mfaVerified ?? false;
254
+ // *** R807: PROPAGATE, DO NOT DEFAULT. *** `?? false` here would destroy the
255
+ // legacy/never-knew distinction on every path through this class - invisibly,
256
+ // because callers construct a SessionModel without restating the field.
257
+ this.mfaSignalPresent = data.mfaSignalPresent;
212
258
  this.mfaMethod = data.mfaMethod;
213
259
  this.mfaCompletedAt = data.mfaCompletedAt;
214
260
  this.mfaExpiresAt = data.mfaExpiresAt;
@@ -266,6 +312,9 @@ export class SessionModel {
266
312
  decodedAccessToken: this.decodedAccessToken,
267
313
  bearerKeyId: this.bearerKeyId,
268
314
  mfaVerified: this.mfaVerified,
315
+ // R807: must survive the Redis round-trip, or the session is re-read as
316
+ // `legacy` one hop later and viability silently stops firing.
317
+ mfaSignalPresent: this.mfaSignalPresent,
269
318
  mfaMethod: this.mfaMethod,
270
319
  mfaCompletedAt: this.mfaCompletedAt,
271
320
  mfaExpiresAt: this.mfaExpiresAt,
@@ -16,9 +16,7 @@
16
16
 
17
17
  'use client';
18
18
 
19
- import React, { useState } from 'react';
20
- import { authClient } from '../../client/better-auth-client';
21
- import { useSearchParams } from 'next/navigation';
19
+ import React from 'react';
22
20
  import { Suspense } from 'react';
23
21
  import { useBranding, useColors } from '../../theme/useTheme';
24
22
 
@@ -36,48 +34,12 @@ interface AdminLoginFormProps {
36
34
  function AdminLoginForm({
37
35
  title = 'Admin Login',
38
36
  subtitle = 'Authorized personnel only',
39
- callbackUrl: propCallbackUrl,
40
37
  logo,
41
38
  }: AdminLoginFormProps) {
42
- const searchParams = useSearchParams();
43
- const callbackUrl = propCallbackUrl || searchParams?.get('callbackUrl') || '/dashboard';
44
-
45
39
  const branding = useBranding();
46
40
  const colors = useColors();
47
-
48
- const [email, setEmail] = useState('');
49
- const [password, setPassword] = useState('');
50
- const [showPassword, setShowPassword] = useState(false);
51
- const [isLoading, setIsLoading] = useState(false);
52
- const [error, setError] = useState<string | null>(null);
53
-
54
- const handleSubmit = async (e: React.FormEvent) => {
55
- e.preventDefault();
56
- setIsLoading(true);
57
- setError(null);
58
-
59
- try {
60
- const result = await authClient.signIn.email({
61
- email,
62
- password,
63
- callbackURL: callbackUrl,
64
- });
65
-
66
- if ((result as any)?.error) {
67
- const errorMsg = typeof (result as any).error === 'object'
68
- ? ((result as any).error as any).message || 'Invalid credentials'
69
- : String((result as any).error);
70
- setError(errorMsg);
71
- } else if (result?.data) {
72
- // Redirect to verify-code for 2FA or directly to callback
73
- window.location.href = `/account-auth/verify-code?callbackUrl=${encodeURIComponent(callbackUrl)}`;
74
- }
75
- } catch (err) {
76
- setError('An unexpected error occurred');
77
- } finally {
78
- setIsLoading(false);
79
- }
80
- };
41
+ void branding;
42
+ void colors;
81
43
 
82
44
  return (
83
45
  <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">
@@ -97,92 +59,20 @@ function AdminLoginForm({
97
59
  {subtitle}
98
60
  </p>
99
61
 
100
- {/* Error */}
101
- {error && (
102
- <div className="mb-6 px-4 py-3 rounded-lg bg-red-500 text-white text-center text-sm">
103
- {error}
104
- </div>
105
- )}
106
-
107
- {/* Login Form */}
108
- <form onSubmit={handleSubmit} className="space-y-4">
109
- <div>
110
- <label
111
- htmlFor="admin-email"
112
- className="block text-sm font-medium mb-2 text-slate-700"
113
- >
114
- Email
115
- </label>
116
- <input
117
- id="admin-email"
118
- type="email"
119
- value={email}
120
- onChange={(e) => setEmail(e.target.value)}
121
- required
122
- disabled={isLoading}
123
- autoComplete="email"
124
- 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"
125
- placeholder="admin@example.com"
126
- />
127
- </div>
128
-
129
- <div>
130
- <label
131
- htmlFor="admin-password"
132
- className="block text-sm font-medium mb-2 text-slate-700"
133
- >
134
- Password
135
- </label>
136
- <div className="relative">
137
- <input
138
- id="admin-password"
139
- type={showPassword ? 'text' : 'password'}
140
- value={password}
141
- onChange={(e) => setPassword(e.target.value)}
142
- required
143
- disabled={isLoading}
144
- autoComplete="current-password"
145
- 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"
146
- placeholder="Enter your password"
147
- />
148
- <button
149
- type="button"
150
- onClick={() => setShowPassword(!showPassword)}
151
- className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-500 hover:text-slate-700"
152
- aria-label={showPassword ? 'Hide password' : 'Show password'}
153
- >
154
- {showPassword ? (
155
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
156
- <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" />
157
- </svg>
158
- ) : (
159
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" className="w-5 h-5">
160
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
161
- <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" />
162
- </svg>
163
- )}
164
- </button>
165
- </div>
166
- </div>
167
-
168
- <button
169
- type="submit"
170
- disabled={isLoading}
171
- 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"
172
- >
173
- {isLoading ? (
174
- <span className="flex items-center justify-center">
175
- <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" fill="none" viewBox="0 0 24 24">
176
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
177
- <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" />
178
- </svg>
179
- Signing in...
180
- </span>
181
- ) : (
182
- 'Sign In'
183
- )}
184
- </button>
185
- </form>
62
+ {/* EMAIL/PASSWORD FORM REMOVED — same root cause as pages/login (see that
63
+ file's identical note): `emailAndPassword` appears ZERO times in
64
+ better-auth.ts, so authClient.signIn.email always returned "Email and
65
+ password is not enabled". Unlike pages/login, THIS page's form was never
66
+ removed when that happened — it kept accepting input and 400ing on every
67
+ submit, real credentials included (QAPert 32622, live-verified against a
68
+ seeded TOTP fixture 2026-08-22). The 222363 accessibility work made the
69
+ error STATE correct; it never made the endpoint real — a dead door that
70
+ looks alive is worse than an absent one (BAPert 32626). Removed 4.1.31.
71
+ To ADD real admin password auth: wire this page to the IDP's password
72
+ endpoint directly (the same one the graft's auth-flow BFF used to call,
73
+ or its package-pattern successor per Jon's B ruling, 32627) — NOT
74
+ authClient.signIn.email, which has never worked and was never wired to
75
+ anything. Restore a form only together with that wiring. */}
186
76
 
187
77
  {/* Security notice */}
188
78
  <p className="mt-6 text-center text-xs text-slate-500">
@@ -10,6 +10,13 @@
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.
13
20
  */
14
21
 
15
22
  'use client';
@@ -195,10 +202,53 @@ function formatRelativeTime(dateString?: string): string {
195
202
  } catch { return dateString; }
196
203
  }
197
204
 
198
- export default function EnhancedSecurityPage() {
205
+ export interface EnhancedSecurityPageProps {
206
+ /**
207
+ * 152745. Where "Enable 2FA" navigates to start enrollment. Deliberately a
208
+ * PROP, not a hardcoded route: this component ships in a shared package
209
+ * consumed by apps whose enrollment routes differ (idp_admin_ui_graft has
210
+ * one at /user-authentication/multi-factor; idealresume — per QAPert's
211
+ * 170047 gate — has NONE at all). Hardcoding either app's route into the
212
+ * package would be wrong for the other, and would silently reproduce the
213
+ * exact "advertises 2FA, nothing reachable" defect this card exists to
214
+ * fix. Undefined -> the button renders honestly disabled (see below),
215
+ * never a fake-live control pointing nowhere.
216
+ */
217
+ mfaEnrollHref?: string;
218
+ /**
219
+ * 152745. Called when "Disable 2FA" is clicked. Left to the consumer for
220
+ * the same reason as mfaEnrollHref: disabling 2FA is a real, consequential
221
+ * write (DELETE /api/Account/mfa/methods/{id} per cluster card 170059) and
222
+ * this package has no opinion on which method id, confirmation flow, or
223
+ * step-up policy a given app requires. Undefined -> honestly disabled.
224
+ */
225
+ onDisable2FA?: () => Promise<void> | void;
226
+ }
227
+
228
+ /**
229
+ * @deprecated RETIRED (BAPert ruling, card 221074, 2026-08-18). Canonical copy:
230
+ * idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
231
+ * This copy receives NO further fixes. Do not import.
232
+ */
233
+ export default function EnhancedSecurityPage({ mfaEnrollHref, onDisable2FA }: EnhancedSecurityPageProps = {}) {
199
234
  const { data: profileData, isLoading: isProfileLoading } = useProfile();
200
235
  const layout = useLayout();
201
236
  const colors = useColors();
237
+ const [disabling2FA, setDisabling2FA] = useState(false);
238
+ const [disable2FAError, setDisable2FAError] = useState('');
239
+
240
+ const handleDisable2FA = async () => {
241
+ if (!onDisable2FA || disabling2FA) return;
242
+ setDisable2FAError('');
243
+ try {
244
+ setDisabling2FA(true);
245
+ await onDisable2FA();
246
+ } catch (err: any) {
247
+ setDisable2FAError(err?.message || 'Failed to disable two-factor authentication');
248
+ } finally {
249
+ setDisabling2FA(false);
250
+ }
251
+ };
202
252
 
203
253
  // Re-auth action types (moved for scope)
204
254
  type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_account' | null;
@@ -437,16 +487,52 @@ type ReAuthAction = 'enable_2fa' | 'disable_2fa' | 'export_data' | 'delete_accou
437
487
  <StatusBadge enabled={twoFactorEnabled} isDark={isDark} />
438
488
  </div>
439
489
  {twoFactorEnabled ? (
440
- <div className="flex gap-2">
441
- <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}`}>
442
- Change Method
443
- </button>
444
- <button className="px-3 py-1.5 text-sm rounded border border-red-500 text-red-500 hover:bg-red-500/10">
445
- Disable 2FA
446
- </button>
490
+ <div className="flex flex-col gap-2">
491
+ <div className="flex gap-2">
492
+ <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}`}>
493
+ Change Method
494
+ </button>
495
+ {/* 152745: was a dead styled button (no onClick/type/href) that
496
+ advertised a working control. Disabling a real 2FA method
497
+ is consequential (DELETE .../mfa/methods/{id}, cluster
498
+ 170059), so this package has no default action — only a
499
+ consumer-supplied onDisable2FA makes it live. Unwired, it
500
+ renders honestly disabled rather than clickable-looking. */}
501
+ <button
502
+ type="button"
503
+ onClick={handleDisable2FA}
504
+ disabled={!onDisable2FA || disabling2FA}
505
+ title={!onDisable2FA ? 'Disabling two-factor authentication is not available here' : undefined}
506
+ 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"
507
+ >
508
+ {disabling2FA ? 'Disabling...' : 'Disable 2FA'}
509
+ </button>
510
+ </div>
511
+ {disable2FAError && <p className="text-xs text-red-400">{disable2FAError}</p>}
447
512
  </div>
513
+ ) : mfaEnrollHref ? (
514
+ // 152745: real navigation to the consumer's own enrollment
515
+ // route (a plain <a>, matching "Back to Profile" below — this
516
+ // package has no opinion on which router the host app uses).
517
+ <a
518
+ href={mfaEnrollHref}
519
+ className="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"
520
+ >
521
+ Enable 2FA
522
+ </a>
448
523
  ) : (
449
- <button className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">
524
+ // 152745: no enrollment destination configured. This is the
525
+ // case QAPert's 170047 gate found live on idealresume.online —
526
+ // rendering an active-looking button here would be the exact
527
+ // "advertises 2FA, nothing reachable" defect this card exists
528
+ // to close, just moved from "no onClick" to "onClick navigates
529
+ // to a 404". Honestly disabled instead.
530
+ <button
531
+ type="button"
532
+ disabled
533
+ title="Two-factor authentication enrollment is not available here"
534
+ className="px-4 py-2 bg-blue-600 text-white rounded-md opacity-40 cursor-not-allowed"
535
+ >
450
536
  Enable 2FA
451
537
  </button>
452
538
  )}
@@ -6,4 +6,7 @@
6
6
  */
7
7
 
8
8
  export { default as SecurityPage } from './page';
9
+ // RETIRED (BAPert ruling, card 221074, 2026-08-18): canonical copy is
10
+ // graft-local at idp_admin_ui_graft/src/app/account/security/EnhancedSecurityPage.tsx.
11
+ /** @deprecated use the graft-local canonical copy (card 221074) */
9
12
  export { default as EnhancedSecurityPage } from './EnhancedSecurityPage';
@@ -4,14 +4,26 @@ import { authClient } from '../../client/better-auth-client';
4
4
  import { useState, useEffect } from 'react';
5
5
 
6
6
  /**
7
- * Emergency Logout Page
7
+ * ⚠️ DESTRUCTIVE DEBUG PAGE — NOT INTENDED FOR PRODUCTION. ⚠️
8
8
  *
9
- * Nuclear option for clearing auth state when things go wrong.
10
- * Clears cookies, localStorage, and forces NextAuth signout.
9
+ * Emergency Logout Page. Nuclear option for clearing auth state when things go
10
+ * wrong: clears cookies, clears localStorage, and forces a signout.
11
11
  *
12
- * Usage in consuming app:
12
+ * ────────────────────────── SCOPE, STATED HONESTLY (QAPert 35000 sweep) ──────────────────────────
13
+ * This is the MILDEST of the four test-env pages and this warning is
14
+ * deliberately weaker than its siblings'. The damage is SELF-INFLICTED and
15
+ * recoverable: a visitor can only destroy their OWN client-side auth state,
16
+ * and the remedy is to sign in again. It exposes no credential and mutates no
17
+ * other user's state. Do not read the banner above as equivalent to the JWT
18
+ * inspector's — inflating a mild finding is how real ones get discounted.
19
+ *
20
+ * It is flagged only because it ships with no env gate of its own, and because
21
+ * clearing localStorage wholesale can discard unrelated application state that
22
+ * a consuming app kept there. Mounting it is the gate.
23
+ *
24
+ * ────────────────────────── IF YOU MOUNT IT, MOUNT IT LIKE THIS ──────────────────────────
13
25
  * ```typescript
14
- * // app/test-env/emergency-logout/page.tsx
26
+ * // app/test-env/emergency-logout/page.tsx — TEST/DEV ENVIRONMENTS ONLY
15
27
  * export { EmergencyLogoutPage as default } from '@payez/next-mvp/pages/test-env';
16
28
  * ```
17
29
  */