@payez/next-mvp 4.1.20 → 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 (177) 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/components/account/UserAvatarMenu.js +12 -0
  31. package/dist/components/auth/ModeAwareLoginPage.js +10 -8
  32. package/dist/components/auth/ModeAwareSignupPage.js +5 -2
  33. package/dist/components/auth/TraditionalAuthSection.js +5 -1
  34. package/dist/hooks/useAuthSettings.js +14 -2
  35. package/dist/hooks/usePublicAuthSettings.d.ts +2 -2
  36. package/dist/hooks/usePublicAuthSettings.js +28 -14
  37. package/dist/lib/client-key.d.ts +61 -0
  38. package/dist/lib/client-key.js +72 -0
  39. package/dist/lib/demo-mode.js +2 -1
  40. package/dist/lib/ensure-fresh-access-token.js +12 -1
  41. package/dist/lib/idp-client-config.d.ts +25 -2
  42. package/dist/lib/idp-client-config.js +381 -36
  43. package/dist/lib/idp-fetch.d.ts +2 -0
  44. package/dist/lib/idp-fetch.js +31 -1
  45. package/dist/lib/idp-url-guard.d.ts +29 -0
  46. package/dist/lib/idp-url-guard.js +38 -0
  47. package/dist/lib/redis.js +77 -5
  48. package/dist/lib/session-cookie-shape.d.ts +69 -0
  49. package/dist/lib/session-cookie-shape.js +126 -0
  50. package/dist/lib/session-store.d.ts +35 -2
  51. package/dist/lib/session-store.js +135 -4
  52. package/dist/lib/single-flight.d.ts +35 -0
  53. package/dist/lib/single-flight.js +30 -0
  54. package/dist/lib/token-lifecycle.js +12 -0
  55. package/dist/middleware/create-middleware.js +12 -0
  56. package/dist/models/SessionModel.d.ts +41 -0
  57. package/dist/models/SessionModel.js +9 -0
  58. package/dist/pages/admin-login/page.d.ts +1 -1
  59. package/dist/pages/admin-login/page.js +5 -41
  60. package/dist/pages/security/EnhancedSecurityPage.d.ts +35 -1
  61. package/dist/pages/security/EnhancedSecurityPage.js +42 -2
  62. package/dist/pages/security/index.d.ts +1 -0
  63. package/dist/pages/security/index.js +3 -0
  64. package/dist/pages/test-env/EmergencyLogoutPage.d.ts +17 -5
  65. package/dist/pages/test-env/EmergencyLogoutPage.js +17 -5
  66. package/dist/pages/test-env/JwtInspectPage.d.ts +30 -5
  67. package/dist/pages/test-env/JwtInspectPage.js +166 -7
  68. package/dist/pages/test-env/RefreshTokenPage.d.ts +22 -6
  69. package/dist/pages/test-env/RefreshTokenPage.js +22 -6
  70. package/dist/pages/test-env/TestEnvPage.d.ts +16 -4
  71. package/dist/pages/test-env/TestEnvPage.js +16 -4
  72. package/dist/pages/test-env/guarded.d.ts +4 -0
  73. package/dist/pages/test-env/guarded.js +86 -0
  74. package/dist/pages/test-env/index.d.ts +50 -7
  75. package/dist/pages/test-env/index.js +58 -12
  76. package/dist/routes/auth/logout.js +3 -2
  77. package/dist/routes/auth/refresh.js +27 -1
  78. package/dist/routes/auth/session.d.ts +12 -3
  79. package/dist/routes/auth/session.js +92 -9
  80. package/dist/routes/auth/settings.d.ts +34 -4
  81. package/dist/routes/auth/settings.js +87 -33
  82. package/dist/routes/auth/viability.d.ts +1 -1
  83. package/dist/routes/auth/viability.js +122 -11
  84. package/dist/server/auth-guard.js +22 -15
  85. package/dist/server/auth.d.ts +170 -4
  86. package/dist/server/auth.js +42 -75
  87. package/dist/server/decode-session.js +84 -10
  88. package/dist/types/auth.d.ts +18 -0
  89. package/package.json +969 -958
  90. package/scripts/check-internal-url-usage.sh +73 -73
  91. package/scripts/check-publish-health.mjs +120 -0
  92. package/scripts/dev-broker.ps1 +71 -6
  93. package/scripts/print-client-config.mjs +162 -0
  94. package/scripts/server-only-stub.cjs +4 -0
  95. package/src/api-handlers/account/recovery/initiate.ts +35 -2
  96. package/src/api-handlers/account/recovery/send-code.ts +36 -2
  97. package/src/api-handlers/account/recovery/verify-code.ts +36 -2
  98. package/src/api-handlers/account/reset-password.ts +35 -2
  99. package/src/api-handlers/account/validate-password.ts +22 -3
  100. package/src/api-handlers/admin/analytics.ts +2 -1
  101. package/src/api-handlers/admin/audit.ts +2 -1
  102. package/src/api-handlers/admin/sessions.ts +2 -1
  103. package/src/api-handlers/admin/stats.ts +2 -1
  104. package/src/api-handlers/admin/users.ts +2 -1
  105. package/src/api-handlers/admin/vibe-data.ts +2 -1
  106. package/src/api-handlers/auth/jwks.ts +10 -0
  107. package/src/api-handlers/auth/login.ts +5 -2
  108. package/src/api-handlers/auth/passkey-signin.ts +112 -98
  109. package/src/api-handlers/auth/refresh.ts +112 -15
  110. package/src/api-handlers/auth/signout.ts +4 -3
  111. package/src/api-handlers/auth/verify-code.ts +5 -2
  112. package/src/api-handlers/mfa/backup-code-verify.ts +175 -175
  113. package/src/api-handlers/mfa/backup-codes.ts +41 -41
  114. package/src/api-handlers/mfa/methods.ts +46 -46
  115. package/src/api-handlers/mfa/totp-confirm.ts +42 -42
  116. package/src/api-handlers/mfa/totp-enroll.ts +42 -42
  117. package/src/api-handlers/passkeys/list.ts +31 -31
  118. package/src/api-handlers/passkeys/manage.ts +54 -54
  119. package/src/api-handlers/passkeys/register-options.ts +42 -42
  120. package/src/api-handlers/passkeys/register-verify.ts +41 -41
  121. package/src/api-handlers/session/viability.ts +29 -1
  122. package/src/api-handlers/test/force-expire.ts +118 -13
  123. package/src/auth/better-auth.ts +486 -51
  124. package/src/auth/utils/idp-client.ts +1 -1
  125. package/src/client/AuthContext.tsx +55 -19
  126. package/src/components/account/UserAvatarMenu.tsx +11 -0
  127. package/src/components/auth/ModeAwareLoginPage.tsx +40 -49
  128. package/src/components/auth/ModeAwareSignupPage.tsx +5 -2
  129. package/src/components/auth/TraditionalAuthSection.tsx +5 -1
  130. package/src/hooks/useAuthSettings.ts +14 -2
  131. package/src/hooks/usePublicAuthSettings.ts +38 -16
  132. package/src/lib/client-key.ts +88 -0
  133. package/src/lib/demo-mode.ts +2 -1
  134. package/src/lib/ensure-fresh-access-token.ts +12 -1
  135. package/src/lib/idp-client-config.ts +399 -36
  136. package/src/lib/idp-fetch.ts +36 -1
  137. package/src/lib/idp-url-guard.ts +37 -0
  138. package/src/lib/redis.ts +84 -6
  139. package/src/lib/session-cookie-shape.ts +184 -0
  140. package/src/lib/session-store.ts +152 -6
  141. package/src/lib/single-flight.ts +64 -0
  142. package/src/lib/token-lifecycle.ts +12 -0
  143. package/src/middleware/create-middleware.ts +11 -0
  144. package/src/models/SessionModel.ts +49 -0
  145. package/src/pages/admin-login/page.tsx +17 -127
  146. package/src/pages/security/EnhancedSecurityPage.tsx +95 -9
  147. package/src/pages/security/index.ts +3 -0
  148. package/src/pages/test-env/EmergencyLogoutPage.tsx +17 -5
  149. package/src/pages/test-env/JwtInspectPage.tsx +235 -20
  150. package/src/pages/test-env/RefreshTokenPage.tsx +22 -6
  151. package/src/pages/test-env/TestEnvPage.tsx +16 -4
  152. package/src/pages/test-env/guarded.tsx +81 -0
  153. package/src/pages/test-env/index.ts +59 -7
  154. package/src/routes/auth/logout.ts +4 -3
  155. package/src/routes/auth/passkey-signin.ts +13 -13
  156. package/src/routes/auth/refresh.ts +30 -1
  157. package/src/routes/auth/session.ts +98 -9
  158. package/src/routes/auth/settings.ts +144 -42
  159. package/src/routes/auth/viability.ts +129 -11
  160. package/src/routes/mfa/backup-code-verify.ts +1 -1
  161. package/src/routes/mfa/backup-codes.ts +10 -10
  162. package/src/routes/mfa/methods.ts +22 -22
  163. package/src/routes/mfa/totp-confirm.ts +10 -10
  164. package/src/routes/mfa/totp-enroll.ts +10 -10
  165. package/src/routes/mfa/totp-verify.ts +14 -14
  166. package/src/routes/passkeys/list.ts +10 -10
  167. package/src/routes/passkeys/manage.ts +23 -23
  168. package/src/routes/passkeys/register-options.ts +10 -10
  169. package/src/routes/passkeys/register-verify.ts +10 -10
  170. package/src/server/auth-guard.ts +24 -17
  171. package/src/server/auth.ts +54 -74
  172. package/src/server/decode-session.ts +95 -11
  173. package/src/templates/instrumentation.ts +21 -2
  174. package/src/types/auth.ts +20 -0
  175. package/dist/lib/auth-secret.d.ts +0 -17
  176. package/dist/lib/auth-secret.js +0 -116
  177. package/src/lib/auth-secret.ts +0 -137
@@ -36,7 +36,7 @@ function getIdpUrl() {
36
36
  * Get client ID for this application.
37
37
  */
38
38
  function getClientId() {
39
- const clientId = process.env.CLIENT_ID || process.env.NEXT_PUBLIC_CLIENT_ID;
39
+ const clientId = process.env.CLIENT_ID;
40
40
  if (!clientId) {
41
41
  throw new Error('[IDP_CLIENT] FATAL: CLIENT_ID environment variable is REQUIRED');
42
42
  }
@@ -32,6 +32,8 @@ const OAUTH_PROVIDER_IDS = ['google', 'apple', 'facebook', 'github', 'azure-ad',
32
32
  function AuthProvider({ children, config, useDynamicProviders = true }) {
33
33
  const [dynamicProviders, setDynamicProviders] = (0, react_1.useState)([]);
34
34
  const [providersLoaded, setProvidersLoaded] = (0, react_1.useState)(!useDynamicProviders);
35
+ // What the IDP says this client offers. null until /api/auth/settings answers.
36
+ const [serverSettings, setServerSettings] = (0, react_1.useState)(null);
35
37
  // Create QueryClient instance for React Query - used internally by MVP hooks
36
38
  const [queryClient] = (0, react_1.useState)(() => new react_query_1.QueryClient({
37
39
  defaultOptions: {
@@ -49,24 +51,45 @@ function AuthProvider({ children, config, useDynamicProviders = true }) {
49
51
  let mounted = true;
50
52
  async function fetchDynamicProviders() {
51
53
  try {
52
- // Better Auth doesn't have a getProviders equivalent.
53
- // Use static provider map based on configured social providers.
54
- const result = {
55
- google: { id: 'google', name: 'Google' },
56
- };
57
- if (!mounted)
54
+ // READ THE IDP. This used to be a hardcoded literal:
55
+ // const result = { google: { id: 'google', name: 'Google' } };
56
+ // sitting under a comment saying providers were loaded dynamically. It returned
57
+ // Google for every client regardless of what the tenant actually had enabled -
58
+ // a config-shaped constant, which is the one thing configuration must never be.
59
+ //
60
+ // /api/auth/settings already serves the real answer (enabledProviders,
61
+ // allowFederatedLogin, enablePasswordReset) and nothing consumed it.
62
+ const response = await fetch('/api/auth/settings');
63
+ if (!response.ok) {
64
+ // No silent substitution. Absent settings means we do not know what this
65
+ // client offers, and inventing a provider list is how a tenant ends up with a
66
+ // sign-in button their IDP will refuse.
67
+ console.error('[AuthProvider] /api/auth/settings returned ' + response.status +
68
+ ' - provider list unknown. Mount ./routes/auth/settings.');
58
69
  return;
59
- if (result) {
60
- // Filter to OAuth providers only and map to FederatedProvider type
61
- const oauthProviders = Object.keys(result)
62
- .filter(id => OAUTH_PROVIDER_IDS.includes(id))
63
- .map(id => PROVIDER_MAP[id])
64
- .filter((p) => p !== undefined);
65
- setDynamicProviders(oauthProviders);
66
70
  }
71
+ const body = await response.json();
72
+ const settings = body?.data ?? body;
73
+ if (!mounted)
74
+ return;
75
+ const ids = Array.isArray(settings?.enabledProviders)
76
+ ? settings.enabledProviders
77
+ : [];
78
+ const oauthProviders = ids
79
+ .filter(id => OAUTH_PROVIDER_IDS.includes(id))
80
+ .map(id => PROVIDER_MAP[id])
81
+ .filter((p) => p !== undefined);
82
+ setDynamicProviders(oauthProviders);
83
+ setServerSettings({
84
+ allowFederatedLogin: settings?.allowFederatedLogin !== false,
85
+ // TRADITIONAL DEFAULTS ON. The IDP does not carry an explicit flag for it yet;
86
+ // until it does, username/password is available unless a site opts out. An
87
+ // OAuth-only client is a deliberate choice, not the default.
88
+ allowTraditionalLogin: settings?.allowTraditionalLogin !== false,
89
+ });
67
90
  }
68
91
  catch (err) {
69
- // Fall back to static config providers on error
92
+ console.error('[AuthProvider] could not read /api/auth/settings:', err);
70
93
  }
71
94
  finally {
72
95
  if (mounted) {
@@ -86,7 +109,12 @@ function AuthProvider({ children, config, useDynamicProviders = true }) {
86
109
  const authConfig = {
87
110
  ...defaultConfig,
88
111
  ...config,
89
- providers, // Override with dynamic providers if enabled
112
+ providers, // from the IDP when useDynamicProviders is on
113
+ // THE SERVER DECIDES, THE SITE MAY OVERRIDE. A site that passes an explicit value
114
+ // still wins; absent that, what the tenant's IDP reports is what renders. A client
115
+ // hardcoding its own auth mode is how a sign-in screen and its IDP disagree.
116
+ allowFederatedLogin: config?.allowFederatedLogin ?? serverSettings?.allowFederatedLogin ?? true,
117
+ allowTraditionalLogin: config?.allowTraditionalLogin ?? serverSettings?.allowTraditionalLogin ?? true,
90
118
  };
91
119
  return ((0, jsx_runtime_1.jsx)(react_query_1.QueryClientProvider, { client: queryClient, children: (0, jsx_runtime_1.jsx)(AuthContext.Provider, { value: authConfig, children: children }) }));
92
120
  }
@@ -107,9 +135,13 @@ function useFederatedProviders() {
107
135
  }
108
136
  function useFederatedAuthEnabled() {
109
137
  const config = useAuthConfig();
110
- return config.mode === 'federated' && config.providers.length > 0;
138
+ // Was `config.mode === 'federated'`, which made the two mutually exclusive and let a
139
+ // site's hardcoded mode silently hide what the IDP offers.
140
+ return config.allowFederatedLogin !== false && config.providers.length > 0;
111
141
  }
112
142
  function useTraditionalAuthEnabled() {
113
143
  const config = useAuthConfig();
114
- return config.mode === 'traditional';
144
+ // Was `config.mode === 'traditional'`. Both can be on at once - that is the normal
145
+ // case, and OAuth-only is the exception a client opts into.
146
+ return config.allowTraditionalLogin !== false;
115
147
  }
@@ -67,6 +67,18 @@ function UserAvatarMenu({ basePath = '', showProfile = true, showSettings = true
67
67
  else {
68
68
  // Use NEXT_PUBLIC env var or default to root
69
69
  const logoutUrl = process.env.NEXT_PUBLIC_LOGOUT_REDIRECT_URL || '/';
70
+ // 272068: revoke server-side BEFORE the better-auth client sign-out clears
71
+ // the cookie jar. authClient.signOut() ALONE leaves the server-side sess
72
+ // record alive - a copied pre-signout cookie still authenticates. The
73
+ // logout route (routes/auth/logout) runs deleteSessionEverywhere and
74
+ // clears the __Secure- twin; it must still see the cookie, so it goes
75
+ // first. Best-effort: a failed revoke must not trap the user on the page.
76
+ try {
77
+ await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
78
+ }
79
+ catch {
80
+ // sign-out continues regardless
81
+ }
70
82
  await better_auth_client_1.authClient.signOut();
71
83
  window.location.href = logoutUrl;
72
84
  }
@@ -27,16 +27,18 @@ function ModeAwareLoginPage({ onFederatedSignIn, onTraditionalSignIn, onForgotPa
27
27
  setLocalLoading(false);
28
28
  }
29
29
  };
30
- const isFederated = config.mode === 'federated';
31
- const isTraditional = config.mode === 'traditional';
30
+ // READ THE FLAGS, NOT `mode`. A single mode made these mutually exclusive, so a site
31
+ // could only ever show ONE of OAuth or username/password. Both are normal together;
32
+ // OAuth-only is the exception a client opts into. Source is the IDP
33
+ // (/api/auth/settings) unless the site deliberately overrides.
34
+ const isFederated = config.allowFederatedLogin !== false && config.providers.length > 0;
35
+ const isTraditional = config.allowTraditionalLogin !== false;
32
36
  const totalLoading = isLoading || localLoading;
33
37
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6 px-4 py-3 rounded-lg", style: {
34
38
  background: 'var(--color-error, #ef4444)',
35
39
  color: 'white',
36
- }, children: error })), isFederated && ((0, jsx_runtime_1.jsx)(FederatedAuthSection_1.FederatedAuthSection, { providers: config.providers, onProviderClick: handleFederatedClick, isLoading: totalLoading })), isTraditional && ((0, jsx_runtime_1.jsx)(TraditionalAuthSection_1.TraditionalAuthSection, { email: email, password: password, onEmailChange: setEmail, onPasswordChange: setPassword, onSubmit: handleTraditionalSubmit, isLoading: totalLoading, buttonText: "Sign In", showForgotPassword: config.enableRecovery, onForgotPassword: onForgotPassword })), isFederated === false && isTraditional === false && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(FederatedAuthSection_1.FederatedAuthSection, { providers: config.providers, onProviderClick: handleFederatedClick, isLoading: totalLoading }), config.providers.length > 0 && ((0, jsx_runtime_1.jsxs)("div", { className: "relative my-6", children: [(0, jsx_runtime_1.jsx)("div", { className: "absolute inset-0 flex items-center", children: (0, jsx_runtime_1.jsx)("div", { className: "w-full", style: {
37
- borderTop: '1px solid var(--border-default)',
38
- } }) }), (0, jsx_runtime_1.jsx)("div", { className: "relative flex justify-center text-sm", children: (0, jsx_runtime_1.jsx)("span", { className: "px-2", style: {
39
- background: 'var(--bg-card)',
40
- color: 'var(--text-secondary)',
41
- }, children: "Or sign in with email" }) })] })), (0, jsx_runtime_1.jsx)(TraditionalAuthSection_1.TraditionalAuthSection, { email: email, password: password, onEmailChange: setEmail, onPasswordChange: setPassword, onSubmit: handleTraditionalSubmit, isLoading: totalLoading, buttonText: "Sign In", showForgotPassword: config.enableRecovery, onForgotPassword: onForgotPassword })] }))] }));
40
+ }, children: error })), isFederated && ((0, jsx_runtime_1.jsx)(FederatedAuthSection_1.FederatedAuthSection, { providers: config.providers, onProviderClick: handleFederatedClick, isLoading: totalLoading })), isFederated && isTraditional && ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 my-6", "aria-hidden": "true", children: [(0, jsx_runtime_1.jsx)("span", { className: "h-px flex-1", style: { background: 'var(--border-default, #e5e7eb)' } }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs uppercase tracking-wide", style: { color: 'var(--text-muted, #6b7280)' }, children: "or" }), (0, jsx_runtime_1.jsx)("span", { className: "h-px flex-1", style: { background: 'var(--border-default, #e5e7eb)' } })] })), isTraditional && ((0, jsx_runtime_1.jsx)(TraditionalAuthSection_1.TraditionalAuthSection, { email: email, password: password, onEmailChange: setEmail, onPasswordChange: setPassword, onSubmit: handleTraditionalSubmit, isLoading: totalLoading, buttonText: "Sign In", showForgotPassword: config.enableRecovery, onForgotPassword: onForgotPassword })), !isFederated && !isTraditional && ((0, jsx_runtime_1.jsxs)("div", { role: "alert", className: "px-4 py-3 rounded-lg text-sm text-left", style: {
41
+ border: '1px solid var(--border-default, #e5e7eb)',
42
+ color: 'var(--text-primary, #111827)',
43
+ }, children: [(0, jsx_runtime_1.jsx)("p", { className: "font-semibold", children: "Sign-in is not available for this application." }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1", children: "No sign-in method is enabled for this client. An administrator needs to enable a federated provider or username and password sign-in." })] }))] }));
42
44
  }
@@ -31,8 +31,11 @@ function ModeAwareSignupPage({ onFederatedSignUp, onTraditionalSignUp, isLoading
31
31
  setLocalLoading(false);
32
32
  }
33
33
  };
34
- const isFederated = config.mode === 'federated';
35
- const isTraditional = config.mode === 'traditional';
34
+ // Same change as ModeAwareLoginPage: read the independent flags, not the single mode.
35
+ // Leaving signup on `mode` would have made sign-in and sign-up disagree about what the
36
+ // client offers - the worse half of a half-migration.
37
+ const isFederated = config.allowFederatedLogin !== false && config.providers.length > 0;
38
+ const isTraditional = config.allowTraditionalLogin !== false;
36
39
  const totalLoading = isLoading || localLoading;
37
40
  return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [error && ((0, jsx_runtime_1.jsx)("div", { className: "mb-6 px-4 py-3 rounded-lg", style: {
38
41
  background: 'var(--color-error, #ef4444)',
@@ -4,7 +4,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  exports.TraditionalAuthSection = TraditionalAuthSection;
5
5
  const jsx_runtime_1 = require("react/jsx-runtime");
6
6
  function TraditionalAuthSection({ email, password, onEmailChange, onPasswordChange, onSubmit, isLoading, buttonText = 'Sign In', showForgotPassword = true, onForgotPassword, }) {
7
- return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: onSubmit, 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", style: { color: 'var(--text-primary)' }, children: "Email Address" }), (0, jsx_runtime_1.jsx)("input", { type: "email", value: email, onChange: (e) => onEmailChange(e.target.value), placeholder: "you@example.com", disabled: isLoading, required: true, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: {
7
+ // text-left is DELIBERATE and load-bearing: a host card with text-align:center made the
8
+ // Email label render CENTRED while Password stayed left, because Password sits in a flex
9
+ // row (for the forgot link) that ignores inherited alignment and Email did not. A shared
10
+ // component must not inherit its alignment from whatever wraps it.
11
+ return ((0, jsx_runtime_1.jsxs)("form", { onSubmit: onSubmit, className: "space-y-4 text-left", children: [(0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("label", { className: "block text-sm font-medium mb-2", style: { color: 'var(--text-primary)' }, children: "Email Address" }), (0, jsx_runtime_1.jsx)("input", { type: "email", value: email, onChange: (e) => onEmailChange(e.target.value), placeholder: "you@example.com", disabled: isLoading, required: true, className: "w-full px-4 py-3 rounded-lg border transition-colors focus:outline-none focus:ring-2 disabled:opacity-50", style: {
8
12
  border: '1px solid var(--border-default)',
9
13
  color: 'var(--text-primary)',
10
14
  background: 'var(--bg-default)',
@@ -56,7 +56,14 @@ function useAuthSettings() {
56
56
  */
57
57
  function useRequires2FA() {
58
58
  const authSettings = useAuthSettings();
59
- return authSettings?.require2FA ?? true; // Default to true for security
59
+ // 257255: DELIBERATELY LEFT FAIL-CLOSED, and flagged rather than silently kept.
60
+ // QAPert proved any hardcoded fallback is wrong in one environment (same client
61
+ // is require_2fa TRUE on AKS, FALSE on dev-93), so `?? true` is not correct - it
62
+ // is merely the SAFE direction: it demands more auth rather than less, and
63
+ // flipping it to false would be fail-OPEN. The genuinely correct answer is to
64
+ // propagate "unknown" to the caller, which is a breaking signature change
65
+ // (boolean -> boolean | null) and needs its own decision. Tracked on 257255.
66
+ return authSettings?.require2FA ?? true;
60
67
  }
61
68
  /**
62
69
  * Hook to get allowed 2FA methods.
@@ -65,7 +72,12 @@ function useRequires2FA() {
65
72
  */
66
73
  function useAllowed2FAMethods() {
67
74
  const authSettings = useAuthSettings();
68
- return authSettings?.allowed2FAMethods ?? ['email', 'sms'];
75
+ // 257255 / v32(a) OFFER NOTHING YOU CANNOT CONFIRM.
76
+ // Was `?? ['email','sms']` - a fabricated method list rendered whenever the
77
+ // policy could not be read. On 2026-08-24 that told users to use email+sms
78
+ // while the database said emailcode+totp. An empty list is the honest answer;
79
+ // the caller must show "we could not load sign-in options" rather than guess.
80
+ return authSettings?.allowed2FAMethods ?? [];
69
81
  }
70
82
  /**
71
83
  * Hook to get session timeout settings.
@@ -7,11 +7,11 @@
7
7
  import type { FederatedProvider } from '../types/auth';
8
8
  export interface PublicAuthSettings {
9
9
  enabledProviders: FederatedProvider[];
10
- allowPublicRegistration: boolean;
11
10
  allowFederatedLogin: boolean;
12
- enablePasswordReset: boolean;
13
11
  require2FA: boolean;
14
12
  allowed2FAMethods: string[];
13
+ allowPublicRegistration?: boolean;
14
+ enablePasswordReset?: boolean;
15
15
  }
16
16
  export interface UsePublicAuthSettingsResult {
17
17
  settings: PublicAuthSettings | null;
@@ -27,14 +27,19 @@ const PROVIDER_MAP = {
27
27
  'microsoft': 'microsoft',
28
28
  'azure-ad': 'microsoft',
29
29
  };
30
- const DEFAULT_SETTINGS = {
31
- enabledProviders: [],
32
- allowPublicRegistration: true,
33
- allowFederatedLogin: false,
34
- enablePasswordReset: true,
35
- require2FA: true,
36
- allowed2FAMethods: ['email', 'sms'],
37
- };
30
+ // 257255 / BAPert ruling v32(a): DEFAULT_SETTINGS DELETED.
31
+ //
32
+ // It was substituted on BOTH failure branches below, so a page kept rendering
33
+ // require2FA=true with methods ['email','sms'] whenever the fetch failed - a
34
+ // FABRICATED AUTHORIZATION POLICY, client-side. Hard-failing the endpoint alone
35
+ // does not fix that: it just moves the lie from the server to the browser.
36
+ //
37
+ // QAPert's measurement makes this a proof rather than a preference: the SAME
38
+ // client is require_2fa=TRUE on AKS and FALSE on dev-93, so any hardcoded
39
+ // fallback is wrong in at least one environment BY CONSTRUCTION.
40
+ //
41
+ // On failure `settings` stays NULL and `error` is set. Callers must render an
42
+ // honest "we could not load sign-in options" state and OFFER NOTHING.
38
43
  /**
39
44
  * Hook to fetch public auth settings for login/signup pages.
40
45
  *
@@ -84,7 +89,12 @@ function usePublicAuthSettings() {
84
89
  });
85
90
  }
86
91
  else {
87
- setSettings(DEFAULT_SETTINGS);
92
+ // The endpoint told us it could not read a policy (503 +
93
+ // success:false + code). Do NOT invent one - surface it.
94
+ const code = data?.error?.code ?? 'POLICY_UNAVAILABLE';
95
+ setSettings(null);
96
+ setError(new Error(`[auth-settings] ${code}: sign-in options unavailable`));
97
+ return;
88
98
  }
89
99
  setError(null);
90
100
  }
@@ -93,7 +103,7 @@ function usePublicAuthSettings() {
93
103
  return;
94
104
  console.warn('[usePublicAuthSettings] Failed to fetch settings:', err);
95
105
  setError(err instanceof Error ? err : new Error(String(err)));
96
- setSettings(DEFAULT_SETTINGS);
106
+ setSettings(null); // 257255: never fabricate a policy
97
107
  }
98
108
  finally {
99
109
  if (mounted) {
@@ -119,13 +129,17 @@ function useFederatedLoginEnabled() {
119
129
  * Hook to check if public registration is enabled.
120
130
  */
121
131
  function usePublicRegistrationEnabled() {
122
- const { settings } = usePublicAuthSettings();
123
- return settings?.allowPublicRegistration ?? true;
132
+ // 257255: was `?? true`, which OFFERED a sign-up lane the IDP may forbid -
133
+ // card 240447 measured exactly that on portal. The field is no longer served
134
+ // (it was never read from config), so this is now always fail-closed until
135
+ // the config document carries it.
136
+ return false;
124
137
  }
125
138
  /**
126
139
  * Hook to check if password reset is enabled.
127
140
  */
128
141
  function usePasswordResetEnabled() {
129
- const { settings } = usePublicAuthSettings();
130
- return settings?.enablePasswordReset ?? true;
142
+ // 257255: was `?? true` and was never read from config. Fail-closed until
143
+ // the config document carries it.
144
+ return false;
131
145
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * ONE resolver for "which client is this request for?" — card 268410.
3
+ *
4
+ * THE ASYMMETRY THIS EXISTS TO SURVIVE (measured at source in the IDP,
5
+ * RequirePayEzUnAuthorizedAttribute.cs):
6
+ *
7
+ * :31-33 int.TryParse succeeds -> Items[PayEzClientId] = numericId; return; NO existence check
8
+ * :41-47 slug -> GetClientIdByNameAsync; if null -> NotFoundResult() VALIDATED
9
+ * :55 no client id at all -> NotFoundResult()
10
+ *
11
+ * So a WRONG SLUG 404s loudly. A WRONG NUMERIC is silently accepted and the
12
+ * request is answered for whatever client that number happens to be. On a
13
+ * multi-tenant identity service that is a WRONG-TENANT ANSWER, not merely a bug.
14
+ *
15
+ * And a stale numeric is the DOCUMENTED NORMAL CASE, not a hypothetical: numeric
16
+ * ids are environment-specific (card 250596 — 79 was dev-93's id for the SAME
17
+ * slug, and the merge collapsed 76 into 12), while the slug is stable across
18
+ * environments.
19
+ *
20
+ * WHY THE FALLBACK WAS SILENT: idp-client-config.ts:462 and :669 resolve
21
+ * `clientSlug: ... ?? ''`. Empty string is FALSY, so the idiom every call site
22
+ * used —
23
+ *
24
+ * config.clientSlug || String(config.clientId)
25
+ *
26
+ * — falls through to the numeric arm whenever the IDP config carries no slug,
27
+ * with nothing emitted to say it happened. Absence collapsed into a
28
+ * usable-looking default: the same class the graft's passkey plugin already
29
+ * called out when `|| ''` made an unset env var look like a valid client_id.
30
+ *
31
+ * WHAT THIS DOES **NOT** DO: it does not flip precedence. Slug still wins, the
32
+ * numeric fallback is still taken, and every deployment that works today keeps
33
+ * working byte-for-byte. Blanket-flipping these to numeric-first is the
34
+ * dangerous-looking-safe change the 268410 ruling explicitly holds.
35
+ *
36
+ * WHAT IT DOES: it makes the dangerous arm ATTRIBUTABLE. Taking the numeric
37
+ * fallback now emits a warning naming the call site, so a wrong-tenant answer
38
+ * is diagnosable in seconds instead of being indistinguishable from a correct
39
+ * one. That ambiguity is what cost hours on S12.
40
+ */
41
+ export interface ClientKeySource {
42
+ clientSlug?: string | null;
43
+ clientId?: string | number | null;
44
+ }
45
+ export interface ResolvedClientKey {
46
+ /** The routing key to send as X-Client-Id / client_id, or null if none resolved. */
47
+ key: string | null;
48
+ /** True when the environment-specific numeric arm was taken. */
49
+ usedNumericFallback: boolean;
50
+ }
51
+ /**
52
+ * Resolve the IDP routing key from a client config.
53
+ *
54
+ * `site` names the caller (e.g. 'admin/analytics') and appears verbatim in the
55
+ * fallback warning — that string is the whole diagnostic value, so pass a real one.
56
+ *
57
+ * Pure and non-throwing: callers decide whether a null key is fatal (refuse) or
58
+ * merely means "omit the header". Both shapes exist in this package and both
59
+ * are correct for their path.
60
+ */
61
+ export declare function resolveClientKey(config: ClientKeySource | null | undefined, site: string): ResolvedClientKey;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /**
3
+ * ONE resolver for "which client is this request for?" — card 268410.
4
+ *
5
+ * THE ASYMMETRY THIS EXISTS TO SURVIVE (measured at source in the IDP,
6
+ * RequirePayEzUnAuthorizedAttribute.cs):
7
+ *
8
+ * :31-33 int.TryParse succeeds -> Items[PayEzClientId] = numericId; return; NO existence check
9
+ * :41-47 slug -> GetClientIdByNameAsync; if null -> NotFoundResult() VALIDATED
10
+ * :55 no client id at all -> NotFoundResult()
11
+ *
12
+ * So a WRONG SLUG 404s loudly. A WRONG NUMERIC is silently accepted and the
13
+ * request is answered for whatever client that number happens to be. On a
14
+ * multi-tenant identity service that is a WRONG-TENANT ANSWER, not merely a bug.
15
+ *
16
+ * And a stale numeric is the DOCUMENTED NORMAL CASE, not a hypothetical: numeric
17
+ * ids are environment-specific (card 250596 — 79 was dev-93's id for the SAME
18
+ * slug, and the merge collapsed 76 into 12), while the slug is stable across
19
+ * environments.
20
+ *
21
+ * WHY THE FALLBACK WAS SILENT: idp-client-config.ts:462 and :669 resolve
22
+ * `clientSlug: ... ?? ''`. Empty string is FALSY, so the idiom every call site
23
+ * used —
24
+ *
25
+ * config.clientSlug || String(config.clientId)
26
+ *
27
+ * — falls through to the numeric arm whenever the IDP config carries no slug,
28
+ * with nothing emitted to say it happened. Absence collapsed into a
29
+ * usable-looking default: the same class the graft's passkey plugin already
30
+ * called out when `|| ''` made an unset env var look like a valid client_id.
31
+ *
32
+ * WHAT THIS DOES **NOT** DO: it does not flip precedence. Slug still wins, the
33
+ * numeric fallback is still taken, and every deployment that works today keeps
34
+ * working byte-for-byte. Blanket-flipping these to numeric-first is the
35
+ * dangerous-looking-safe change the 268410 ruling explicitly holds.
36
+ *
37
+ * WHAT IT DOES: it makes the dangerous arm ATTRIBUTABLE. Taking the numeric
38
+ * fallback now emits a warning naming the call site, so a wrong-tenant answer
39
+ * is diagnosable in seconds instead of being indistinguishable from a correct
40
+ * one. That ambiguity is what cost hours on S12.
41
+ */
42
+ Object.defineProperty(exports, "__esModule", { value: true });
43
+ exports.resolveClientKey = resolveClientKey;
44
+ /**
45
+ * Resolve the IDP routing key from a client config.
46
+ *
47
+ * `site` names the caller (e.g. 'admin/analytics') and appears verbatim in the
48
+ * fallback warning — that string is the whole diagnostic value, so pass a real one.
49
+ *
50
+ * Pure and non-throwing: callers decide whether a null key is fatal (refuse) or
51
+ * merely means "omit the header". Both shapes exist in this package and both
52
+ * are correct for their path.
53
+ */
54
+ function resolveClientKey(config, site) {
55
+ const slug = typeof config?.clientSlug === 'string' ? config.clientSlug.trim() : '';
56
+ if (slug)
57
+ return { key: slug, usedNumericFallback: false };
58
+ const rawNumeric = config?.clientId;
59
+ // Guard String(undefined) === 'undefined' — a 13-char string that is not a
60
+ // slug and not a number, so the IDP would 404 it with no hint as to why.
61
+ if (rawNumeric === null || rawNumeric === undefined) {
62
+ return { key: null, usedNumericFallback: false };
63
+ }
64
+ const numeric = String(rawNumeric).trim();
65
+ if (!numeric)
66
+ return { key: null, usedNumericFallback: false };
67
+ console.warn(`[client-key] ${site}: no clientSlug on the resolved IDP config — falling back to the ` +
68
+ `NUMERIC client id ${numeric}. Numeric ids are ENVIRONMENT-SPECIFIC and the IDP accepts ` +
69
+ `them with NO existence check, so if this number is stale for this environment the ` +
70
+ `request is being answered for the WRONG CLIENT, silently. Card 268410.`);
71
+ return { key: numeric, usedNumericFallback: true };
72
+ }
@@ -14,5 +14,6 @@ function isAuthConfigured() {
14
14
  return false;
15
15
  return !!(process.env.BETTER_AUTH_SECRET ||
16
16
  process.env.NEXTAUTH_SECRET ||
17
- (process.env.NEXT_CLIENT_ID && process.env.NEXT_CLIENT_PRIVATE_KEY_PEM));
17
+ // was NEXT_CLIENT_ID - a third name for CLIENT_ID, read only here. Collapsed 2026-08-26.
18
+ (process.env.CLIENT_ID && process.env.NEXT_CLIENT_PRIVATE_KEY_PEM));
18
19
  }
@@ -50,6 +50,13 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
50
50
  }
51
51
  const storedToken = session.idpAccessToken;
52
52
  if (!storedToken) {
53
+ // Card 187871: same conflation as getIdpToken (server/auth.ts) - a live
54
+ // session with no access token and mfaVerified !== true is a user
55
+ // mid-2FA, not dead. Only terminal once the session itself claims 2FA
56
+ // is already done.
57
+ if (session.mfaVerified !== true) {
58
+ return { ok: false, code: 'NO_TOKEN_PENDING_2FA', message: 'Two-factor authentication required', status: 401, terminal: false };
59
+ }
53
60
  return { ok: false, code: 'NO_TOKEN', message: 'No IDP access token in session', status: 401, terminal: true };
54
61
  }
55
62
  const now = Date.now();
@@ -304,7 +311,11 @@ async function ensureFreshAccessToken(sessionToken, config, options = {}) {
304
311
  bearerKeyId: newKid || session.bearerKeyId,
305
312
  authenticationMethods: amrClaims,
306
313
  authenticationLevel: acrLevel,
307
- mfaVerified: amrClaims.includes('mfa') || session.mfaVerified,
314
+ // 170007 rule 2 (BAPert ruling on 121350): re-derive from the token
315
+ // every exchange, never carry forward. See the identical fix + full
316
+ // rationale in api-handlers/auth/refresh.ts's sessionUpdate — this is
317
+ // the same defect duplicated at a second call site.
318
+ mfaVerified: amrClaims.includes('mfa'),
308
319
  mfaCompletedAt: decoded?.mfa_time ? parseInt(decoded.mfa_time) * 1000 : session.mfaCompletedAt,
309
320
  mfaExpiresAt: decoded?.mfa_expires ? parseInt(decoded.mfa_expires) * 1000 : session.mfaExpiresAt,
310
321
  mfaValidityHours: decoded?.mfa_validity_hours ? parseInt(decoded.mfa_validity_hours) : session.mfaValidityHours,
@@ -27,8 +27,8 @@ export interface OAuthProviderConfig {
27
27
  additionalParams?: Record<string, any>;
28
28
  }
29
29
  export interface AuthSettings {
30
- require2FA: boolean;
31
- allowed2FAMethods: string[];
30
+ require2FA?: boolean;
31
+ allowed2FAMethods?: string[];
32
32
  mfaGracePeriodHours: number;
33
33
  mfaRememberDeviceDays: number;
34
34
  sessionTimeoutMinutes: number;
@@ -37,6 +37,29 @@ export interface AuthSettings {
37
37
  rememberMeDays: number;
38
38
  lockoutThreshold: number;
39
39
  lockoutDurationMinutes: number;
40
+ /**
41
+ * 240447: tenant policy the IDP has always sent and this loader never read.
42
+ *
43
+ * The IDP emits these inside `auth_settings` on the internal client-config
44
+ * response (PayEz.Identity.Api InternalClientConfigController, the
45
+ * `auth_settings` object: allow_public_registration, allow_social_login,
46
+ * enable_password_reset — backed by IDPClientSecurityPolicy).
47
+ * routes/auth/settings was answering with hardcoded `true` literals
48
+ * carrying the comment "Could come from config.authSettings in future",
49
+ * so the portal advertised self-serve registration for a tenant whose IDP
50
+ * forbids it (measured same-client same-minute: IDP
51
+ * allow_public_registration=FALSE, portal allowPublicRegistration=TRUE).
52
+ *
53
+ * OPTIONAL ON PURPOSE — `undefined` must stay distinguishable from
54
+ * `false`. undefined = the IDP did not tell us (older IDP build, or
55
+ * auth_settings absent entirely); false = the IDP affirmatively said no.
56
+ * Collapsing the first into a boolean default is exactly how the previous
57
+ * hardcoded `true` became a lie that read like a measurement. Consumers
58
+ * must decide explicitly what an unknown means for them.
59
+ */
60
+ allowPublicRegistration?: boolean;
61
+ allowSocialLogin?: boolean;
62
+ enablePasswordReset?: boolean;
40
63
  }
41
64
  export interface BrandingConfig {
42
65
  theme?: string;