@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
@@ -47,6 +47,7 @@ const session_store_1 = require("../lib/session-store");
47
47
  const idp_client_config_1 = require("../lib/idp-client-config");
48
48
  const app_slug_1 = require("../lib/app-slug");
49
49
  const startup_init_1 = require("../lib/startup-init");
50
+ const session_cookie_shape_1 = require("../lib/session-cookie-shape");
50
51
  /**
51
52
  * Try Better Auth's server-side session API.
52
53
  * Returns a DecodedSession if Better Auth has an active session, null otherwise.
@@ -126,6 +127,43 @@ async function tryBetterAuthSession(requestCookies) {
126
127
  }
127
128
  }
128
129
  catch { /* Redis unavailable */ }
130
+ // LEG-1 FAIL-CLOSED ON UNUSABLE ROLES (card 340015, BAPert R412, 2026-09-03).
131
+ //
132
+ // `idpTokens.roles` is the ONLY source of roles for every role-gated route
133
+ // (see :109 below). When it is empty this leg used to return a perfectly
134
+ // valid-LOOKING session carrying `roles: []` — so the caller was
135
+ // authenticated but authorised for nothing. Every admin screen 403s and
136
+ // renders its chrome with no data and NO ERROR TEXT: it does not look
137
+ // broken, it looks empty.
138
+ //
139
+ // Three independent causes produce that state, so the test is on the
140
+ // CONDITION THAT ACTUALLY HARMS — "no usable roles" — rather than on any
141
+ // one route to it. A fourth cause is then covered for free:
142
+ // A MINT the password / admin-login lane never writes the block
143
+ // (it never calls storeIdpTokensInSession), so it is
144
+ // absent from birth.
145
+ // B CLOBBER better-auth's own 24h session refresh rebuilds the
146
+ // `ba:` record as `{session, user}` and drops the
147
+ // `idpTokens` SIBLING structurally, not conditionally
148
+ // (better-auth internal-adapter.mjs:287). This hits the
149
+ // OAuth and passkey lanes too — they are safe for 24
150
+ // hours and then stop being safe.
151
+ // C DERIVATION the block is PRESENT and fully populated with only
152
+ // `roles` empty (card 339748). `!idpTokens` is FALSE
153
+ // here and cannot see it — which is exactly why the
154
+ // predicate is `!idpTokens?.roles?.length` and not an
155
+ // absence check.
156
+ //
157
+ // Declining here hands the request to the legacy `{slug}:sess:` leg below,
158
+ // which carries the same roles. *** IF THAT RECORD IS ALSO ABSENT THE USER
159
+ // IS SIGNED OUT. THAT IS DELIBERATE AND ACCEPTED, NOT A REGRESSION *** —
160
+ // an honest, visible sign-out in place of a silently half-authorised
161
+ // session. Read the commit message before treating a sign-out here as a
162
+ // bug introduced by this fix.
163
+ if (!idpTokens?.roles?.length) {
164
+ console.warn('[DECODE-SESSION] Leg 1 declining: idpTokens unusable (no roles) for session', result.session.token.substring(0, 10), '— falling through to the sess: leg');
165
+ return null;
166
+ }
129
167
  // Map Better Auth session + IDP tokens to SessionData
130
168
  const sessionData = {
131
169
  userId: idpTokens?.userId || result.user.id || '',
@@ -137,6 +175,9 @@ async function tryBetterAuthSession(requestCookies) {
137
175
  idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
138
176
  || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
139
177
  mfaVerified: idpTokens?.mfaVerified ?? false,
178
+ // R807 (362550): fail-closed collapse above stays; this preserves the bit it
179
+ // destroys. Explicit `false` counts as a signal - hence `!== undefined`.
180
+ mfaSignalPresent: idpTokens?.mfaVerified !== undefined,
140
181
  /**
141
182
  * ⚠ WAS HARDCODED `'google'` — third site of QAPert finding #3 (card
142
183
  * 167892), found by sweeping rather than by report.
@@ -207,23 +248,56 @@ async function decodeSession(requestCookies) {
207
248
  console.error('[DECODE-SESSION] No authSecret available from IDP config');
208
249
  return null;
209
250
  }
210
- const secretKey = new TextEncoder().encode(secret);
251
+ // BA-SHAPE-AWARE FALLBACK LEG (183047, 2026-08-08).
252
+ //
253
+ // Better Auth mints session cookies as `{token}.{base64(hmac)}` — TWO
254
+ // segments, one dot. jose's `jwtVerify()` below requires a genuine
255
+ // compact JWS — THREE segments — and throws "Invalid Compact JWS" on a
256
+ // BA-shaped cookie unconditionally, before it ever looks at the secret.
257
+ // Since Better Auth is the only minter in production, that meant this
258
+ // fallback could never succeed for the cookies it actually receives: any
259
+ // single Better-Auth-primary miss (secondary-storage eviction, disabled
260
+ // cookie cache, in-flight secret divergence) read as total session
261
+ // death, because the ONLY fallback was structurally blind to the only
262
+ // shape it would ever be handed.
263
+ //
264
+ // Check the shape FIRST. A BA-hmac cookie skips jose entirely and goes
265
+ // straight to the Redis-canonical liveness check below — same posture
266
+ // as the JWS path always had (no signature re-check here either; Redis
267
+ // presence is the authority). A JWS-shaped cookie (legacy NextAuth-era,
268
+ // if any still linger) keeps the original jwtVerify path unchanged.
269
+ const shaped = (0, session_cookie_shape_1.extractSessionTokenFromCookieValue)(cookieValue);
270
+ let sessionToken;
211
271
  let payload;
212
- try {
213
- const result = await (0, jose_1.jwtVerify)(cookieValue, secretKey);
214
- payload = result.payload;
272
+ if (shaped?.shape === 'ba-hmac') {
273
+ sessionToken = shaped.token;
215
274
  }
216
- catch (jwtError) {
217
- console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
218
- return null;
275
+ else {
276
+ const secretKey = new TextEncoder().encode(secret);
277
+ try {
278
+ const result = await (0, jose_1.jwtVerify)(cookieValue, secretKey);
279
+ payload = result.payload;
280
+ }
281
+ catch (jwtError) {
282
+ console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
283
+ return null;
284
+ }
285
+ sessionToken = payload.sessionToken || payload.redisSessionId;
219
286
  }
220
- const sessionToken = payload.sessionToken || payload.redisSessionId;
221
287
  if (!sessionToken) {
222
- console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
288
+ console.warn('[DECODE-SESSION] No sessionToken resolved from cookie (shape:', shaped?.shape ?? 'unrecognized', ')');
223
289
  return null;
224
290
  }
225
291
  const sessionData = await (0, session_store_1.getSession)(sessionToken);
226
292
  if (!sessionData) {
293
+ // The OTHER half of R412(ii). There are two ways decodeSession returns
294
+ // null and they mean different things, so they must not both be silent:
295
+ // Leg 1 declining is routine and recoverable (this leg normally holds
296
+ // the roles), whereas reaching HERE means there is no fallback record
297
+ // either and the caller is about to be signed out. Different meanings,
298
+ // different remediations — do not ship a second silent path while
299
+ // fixing a silent path.
300
+ console.warn('[DECODE-SESSION] No sess: record for token', sessionToken.substring(0, 10), '— no fallback available, session will be signed out');
227
301
  return null;
228
302
  }
229
303
  // Backwards compat: session.user.email works alongside session.email
@@ -237,7 +311,7 @@ async function decodeSession(requestCookies) {
237
311
  }
238
312
  return {
239
313
  sessionData,
240
- jwtPayload: payload,
314
+ jwtPayload: (payload ?? { sessionToken }),
241
315
  };
242
316
  }
243
317
  catch (error) {
@@ -2,7 +2,25 @@ import { JwtPayload } from 'jwt-decode';
2
2
  export type AuthMode = 'traditional' | 'federated';
3
3
  export type FederatedProvider = 'google' | 'microsoft' | 'facebook' | 'github' | 'apple';
4
4
  export interface AuthConfig {
5
+ /**
6
+ * @deprecated Superseded by allowFederatedLogin / allowTraditionalLogin.
7
+ * A single mode made the two mutually exclusive, so a site could only ever show ONE of
8
+ * OAuth or username/password - and because sites set it themselves, a client's sign-in
9
+ * screen could contradict what its own IDP offered. Kept so existing callers compile.
10
+ */
5
11
  mode: AuthMode;
12
+ /**
13
+ * Whether federated (OAuth) sign-in is offered. Defaults to what the IDP reports at
14
+ * /api/auth/settings; a site may override. Both this and allowTraditionalLogin can be
15
+ * true at once - that is the normal case.
16
+ */
17
+ allowFederatedLogin?: boolean;
18
+ /**
19
+ * Whether username/password sign-in is offered. DEFAULTS ON. The IDP does not carry an
20
+ * explicit flag for it yet; until it does, it is available unless a site opts out.
21
+ * An OAuth-only client is a deliberate choice, never the default.
22
+ */
23
+ allowTraditionalLogin?: boolean;
6
24
  providers: FederatedProvider[];
7
25
  enableRecovery: boolean;
8
26
  enableEmailSignup: boolean;