@payez/next-mvp 4.1.14 → 4.1.21

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 (116) hide show
  1. package/dist/api-handlers/account/profile.d.ts +38 -0
  2. package/dist/api-handlers/account/profile.js +73 -45
  3. package/dist/api-handlers/account/recovery/send-code.d.ts +33 -0
  4. package/dist/api-handlers/account/recovery/send-code.js +52 -18
  5. package/dist/api-handlers/account/recovery/verify-code.d.ts +33 -0
  6. package/dist/api-handlers/account/recovery/verify-code.js +52 -18
  7. package/dist/api-handlers/account/verify-email.js +15 -1
  8. package/dist/api-handlers/account/verify-sms.js +15 -1
  9. package/dist/api-handlers/auth/passkey-signin.d.ts +29 -0
  10. package/dist/api-handlers/auth/passkey-signin.js +97 -0
  11. package/dist/api-handlers/auth/refresh.js +38 -15
  12. package/dist/api-handlers/mfa/backup-code-verify.d.ts +73 -0
  13. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  14. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  15. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  16. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  17. package/dist/api-handlers/mfa/methods.js +41 -0
  18. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  19. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  20. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  21. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  22. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  23. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  24. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  25. package/dist/api-handlers/passkeys/list.js +29 -0
  26. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  27. package/dist/api-handlers/passkeys/manage.js +54 -0
  28. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  29. package/dist/api-handlers/passkeys/register-options.js +41 -0
  30. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  31. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  32. package/dist/api-handlers/session/viability.js +19 -0
  33. package/dist/auth/better-auth.d.ts +68 -2
  34. package/dist/auth/better-auth.js +137 -18
  35. package/dist/auth/utils/idp-client.d.ts +13 -22
  36. package/dist/auth/utils/idp-client.js +12 -82
  37. package/dist/client/better-auth-client.d.ts +62 -62
  38. package/dist/lib/ensure-fresh-access-token.js +49 -8
  39. package/dist/lib/session-store.d.ts +1 -0
  40. package/dist/lib/session-store.js +19 -8
  41. package/dist/lib/token-lifecycle.d.ts +2 -0
  42. package/dist/lib/token-lifecycle.js +61 -7
  43. package/dist/models/SessionModel.d.ts +17 -0
  44. package/dist/pages/login/page.d.ts +10 -1
  45. package/dist/pages/login/page.js +69 -80
  46. package/dist/pages/verify-code/page.d.ts +10 -1
  47. package/dist/pages/verify-code/page.js +72 -4
  48. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  49. package/dist/routes/auth/passkey-signin.js +18 -0
  50. package/dist/routes/auth/viability.d.ts +2 -0
  51. package/dist/routes/auth/viability.js +88 -0
  52. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  53. package/dist/routes/mfa/backup-code-verify.js +5 -0
  54. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  55. package/dist/routes/mfa/backup-codes.js +14 -0
  56. package/dist/routes/mfa/methods.d.ts +21 -0
  57. package/dist/routes/mfa/methods.js +10 -0
  58. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  59. package/dist/routes/mfa/totp-confirm.js +14 -0
  60. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  61. package/dist/routes/mfa/totp-enroll.js +14 -0
  62. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  63. package/dist/routes/mfa/totp-verify.js +18 -0
  64. package/dist/routes/passkeys/list.d.ts +10 -0
  65. package/dist/routes/passkeys/list.js +14 -0
  66. package/dist/routes/passkeys/manage.d.ts +18 -0
  67. package/dist/routes/passkeys/manage.js +13 -0
  68. package/dist/routes/passkeys/register-options.d.ts +10 -0
  69. package/dist/routes/passkeys/register-options.js +14 -0
  70. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  71. package/dist/routes/passkeys/register-verify.js +14 -0
  72. package/dist/server/auth.d.ts +29 -20
  73. package/dist/server/auth.js +8 -0
  74. package/dist/server/decode-session.js +37 -3
  75. package/package.json +958 -894
  76. package/scripts/check-internal-url-usage.sh +73 -73
  77. package/src/api-handlers/account/profile.ts +76 -57
  78. package/src/api-handlers/account/recovery/send-code.ts +65 -17
  79. package/src/api-handlers/account/recovery/verify-code.ts +65 -17
  80. package/src/api-handlers/account/verify-email.ts +15 -1
  81. package/src/api-handlers/account/verify-sms.ts +15 -1
  82. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  83. package/src/api-handlers/auth/refresh.ts +38 -15
  84. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  85. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  86. package/src/api-handlers/mfa/methods.ts +46 -0
  87. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  88. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  89. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  90. package/src/api-handlers/passkeys/list.ts +31 -0
  91. package/src/api-handlers/passkeys/manage.ts +54 -0
  92. package/src/api-handlers/passkeys/register-options.ts +42 -0
  93. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  94. package/src/api-handlers/session/viability.ts +19 -0
  95. package/src/auth/better-auth.ts +158 -18
  96. package/src/auth/utils/idp-client.ts +13 -95
  97. package/src/lib/ensure-fresh-access-token.ts +49 -8
  98. package/src/lib/session-store.ts +11 -1
  99. package/src/lib/token-lifecycle.ts +70 -7
  100. package/src/models/SessionModel.ts +18 -0
  101. package/src/pages/login/page.tsx +123 -228
  102. package/src/pages/verify-code/page.tsx +71 -5
  103. package/src/routes/auth/passkey-signin.ts +13 -0
  104. package/src/routes/auth/viability.ts +89 -0
  105. package/src/routes/mfa/backup-code-verify.ts +1 -0
  106. package/src/routes/mfa/backup-codes.ts +10 -0
  107. package/src/routes/mfa/methods.ts +22 -0
  108. package/src/routes/mfa/totp-confirm.ts +10 -0
  109. package/src/routes/mfa/totp-enroll.ts +10 -0
  110. package/src/routes/mfa/totp-verify.ts +14 -0
  111. package/src/routes/passkeys/list.ts +10 -0
  112. package/src/routes/passkeys/manage.ts +23 -0
  113. package/src/routes/passkeys/register-options.ts +10 -0
  114. package/src/routes/passkeys/register-verify.ts +10 -0
  115. package/src/server/auth.ts +17 -1
  116. package/src/server/decode-session.ts +36 -3
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Ready-to-Use Passkey Sign-In Route (ANONYMOUS ceremony)
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkey/sign-in/options/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/auth/passkey-signin';
8
+ * // (optionsPOST is exported as POST; mount verify at its own path:)
9
+ * // app/api/passkey/sign-in/verify/route.ts
10
+ * export { verifyPOST as POST } from '@payez/next-mvp/routes/auth/passkey-signin';
11
+ * ```
12
+ */
13
+ export { optionsPOST, verifyPOST } from '../../api-handlers/auth/passkey-signin';
@@ -35,6 +35,60 @@ async function getTenantRequiresTwoFactor(): Promise<boolean> {
35
35
  }
36
36
  }
37
37
 
38
+ /**
39
+ * Which 2FA methods the TENANT permits — cards 141688 + 141741.
40
+ *
41
+ * WHY THIS IS HERE: this endpoint answered "is 2FA required, and is it done?"
42
+ * and said nothing about WITH WHAT. Both the middleware gate and the
43
+ * verify-code page call this route, and NEITHER had any method data — so
44
+ * nothing in the system asked whether the user CAN complete the challenge it
45
+ * is about to demand. A user whose tenant allows only `totp`, with no
46
+ * authenticator enrolled, is sent to a page that offers email and sms.
47
+ *
48
+ * ⚠ THIS IS NOT A COMPLETION SIGNAL AND MUST NEVER BE READ AS ONE.
49
+ * It says what the tenant PERMITS. It says nothing about what the user has
50
+ * enrolled, and nothing about whether they have verified. The invariant
51
+ * (QAPert, acceptance J2/J5) is: METHOD-PRESENCE IS NOT VERIFIED. A user with
52
+ * one method is still challenged and still gated until they actually verify.
53
+ * `userHasCompletedTenantTwoFactorRequirements` remains the only field that
54
+ * answers "are they through".
55
+ *
56
+ * STILL MISSING, deliberately not guessed here: the ENROLLED set. That lives
57
+ * behind /api/mfa/methods and needs a call this route does not currently make.
58
+ * Allowed-only closes the "offered a method the tenant forbids" half; it does
59
+ * NOT close "has nothing enrolled and no way to enrol". Both halves are named
60
+ * in the cards so neither gets lost.
61
+ *
62
+ * NULL MEANS UNKNOWN — IT DOES NOT MEAN "NONE", AND IT DOES NOT MEAN
63
+ * "email and sms". If the client config cannot be read we do not know what the
64
+ * tenant permits, and every available guess is wrong in a way that matters:
65
+ *
66
+ * - guessing ['email','sms'] fails OPEN. On a totp-only tenant that offers
67
+ * the user two methods the tenant forbids — which is defect 141688 itself,
68
+ * re-created by its own fix during a config outage.
69
+ * - guessing [] fails CLOSED but locks the user out of a challenge they may
70
+ * be perfectly able to complete.
71
+ *
72
+ * So we report the truth (we don't know) and let the consumer decide, rather
73
+ * than encoding a guess that reads like a measurement. Same reasoning as the
74
+ * nullable require_2fa fix: "absent" has to be expressible, or callers cannot
75
+ * tell a real value from a default.
76
+ *
77
+ * No local default: `allowed2FAMethods` is non-optional on AuthSettings and is
78
+ * populated on BOTH config construction paths (idp-client-config.ts:333, :514)
79
+ * with the tenant's value or the layer's own ['email','sms']. Repeating that
80
+ * default here would be a second source for one setting, free to drift.
81
+ */
82
+ async function getTenantAllowed2FAMethods(): Promise<string[] | null> {
83
+ try {
84
+ const config = await getIDPClientConfig();
85
+ return config.authSettings.allowed2FAMethods;
86
+ } catch {
87
+ console.warn('[VIABILITY] Could not get client config — reporting allowed 2FA methods as UNKNOWN (null), not guessing');
88
+ return null;
89
+ }
90
+ }
91
+
38
92
  /**
39
93
  * GET /api/session/viability - Check if session is viable
40
94
  *
@@ -86,6 +140,31 @@ export async function GET(req: NextRequest) {
86
140
  });
87
141
  }
88
142
 
143
+ // Cards 141688/141741 — the IDP's routing decision, relayed verbatim.
144
+ // See the SessionData.nextStep doc. This is the field that tells the gate
145
+ // whether the user CAN complete a challenge; without it both consumers were
146
+ // sending zero-method users to a page they had no way to act on.
147
+ //
148
+ // Declared once, here, above every return: `session` is settled by this
149
+ // point, and all three authenticated responses must carry the SAME value.
150
+ // Deriving it per-branch would let the paths drift apart silently.
151
+ //
152
+ // NO `?? null` HERE, DELIBERATELY — and this is the whole safety argument:
153
+ //
154
+ // undefined = we never captured it. Every session minted BEFORE this
155
+ // change is in that state, and there are live ones in Redis
156
+ // right now. JSON.stringify omits the key, so the consumer
157
+ // sees no field and knows it is uninformed.
158
+ // null = the IDP affirmatively said "nothing outstanding".
159
+ //
160
+ // Collapsing the first into the second with `?? null` would tell the gate
161
+ // "fully authenticated, nothing to do" about every pre-existing session —
162
+ // including a zero-method user who must be sent to enrol. That is a
163
+ // fail-open on the entire existing session population, produced by a
164
+ // default that reads like a measurement. Same mistake as the nullable
165
+ // require_2fa case: absent has to stay expressible.
166
+ const nextStep = session?.nextStep;
167
+
89
168
  // Check access token expiry
90
169
  const now = Math.floor(Date.now() / 1000);
91
170
  const accessTokenExpires = (token as any).accessTokenExpires || token.exp;
@@ -93,6 +172,8 @@ export async function GET(req: NextRequest) {
93
172
  if (!accessTokenExpires) {
94
173
  // No expiry info, assume viable but recommend refresh
95
174
  const tenantRequiresTwoFactor = await getTenantRequiresTwoFactor();
175
+ // Cards 141688/141741 — see getTenantAllowed2FAMethods. NOT a completion signal.
176
+ const tenantAllowed2FAMethods = await getTenantAllowed2FAMethods();
96
177
 
97
178
  // CRITICAL: Check if MFA has expired (2FA TTL enforcement)
98
179
  const mfaExpiresAt = session?.mfaExpiresAt || 0;
@@ -115,6 +196,8 @@ export async function GET(req: NextRequest) {
115
196
  tenantRequiresTwoFactor,
116
197
  userHasCompletedTenantTwoFactorRequirements,
117
198
  userStillNeedsTwoFactor,
199
+ tenantAllowed2FAMethods,
200
+ nextStep,
118
201
  // Legacy field names for backwards compatibility
119
202
  requires2FA: tenantRequiresTwoFactor,
120
203
  twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
@@ -137,6 +220,8 @@ export async function GET(req: NextRequest) {
137
220
 
138
221
  // CLEAR NAMING: Tenant-wide 2FA requirement from client config
139
222
  const tenantRequiresTwoFactor = await getTenantRequiresTwoFactor();
223
+ // Cards 141688/141741 — see getTenantAllowed2FAMethods. NOT a completion signal.
224
+ const tenantAllowed2FAMethods = await getTenantAllowed2FAMethods();
140
225
 
141
226
  // CRITICAL: Check if MFA has expired (2FA TTL enforcement)
142
227
  // The session may have mfaVerified=true from days ago, but if mfaExpiresAt
@@ -181,6 +266,8 @@ export async function GET(req: NextRequest) {
181
266
  tenantRequiresTwoFactor,
182
267
  userHasCompletedTenantTwoFactorRequirements,
183
268
  userStillNeedsTwoFactor,
269
+ tenantAllowed2FAMethods,
270
+ nextStep,
184
271
  // Legacy names for backwards compatibility
185
272
  requires2FA: tenantRequiresTwoFactor,
186
273
  twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
@@ -203,6 +290,8 @@ export async function GET(req: NextRequest) {
203
290
  tenantRequiresTwoFactor,
204
291
  userHasCompletedTenantTwoFactorRequirements,
205
292
  userStillNeedsTwoFactor,
293
+ tenantAllowed2FAMethods,
294
+ nextStep,
206
295
  // Legacy names for backwards compatibility
207
296
  requires2FA: tenantRequiresTwoFactor,
208
297
  twoFactorComplete: userHasCompletedTenantTwoFactorRequirements,
@@ -0,0 +1 @@
1
+ export { POST } from '../../api-handlers/mfa/backup-code-verify';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use MFA Backup Codes Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/mfa/backup-codes/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/mfa/backup-codes';
8
+ * ```
9
+ */
10
+ export { POST } from '../../api-handlers/mfa/backup-codes';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Ready-to-Use MFA Methods Route (list / remove)
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/mfa/methods/route.ts
7
+ * export { GET } from '@payez/next-mvp/routes/mfa/methods';
8
+ * // app/api/mfa/methods/[id]/route.ts
9
+ * export { DELETE } from '@payez/next-mvp/routes/mfa/methods';
10
+ * ```
11
+ */
12
+ import { NextRequest } from 'next/server';
13
+ import { GET as listMethods, DELETE as deleteMethod } from '../../api-handlers/mfa/methods';
14
+
15
+ export const GET = listMethods;
16
+
17
+ type Ctx = { params: Promise<{ id: string }> };
18
+
19
+ export async function DELETE(req: NextRequest, ctx: Ctx) {
20
+ const { id } = await ctx.params;
21
+ return deleteMethod(req, id);
22
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use TOTP Confirm Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/mfa/totp/confirm/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/mfa/totp-confirm';
8
+ * ```
9
+ */
10
+ export { POST } from '../../api-handlers/mfa/totp-confirm';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use TOTP Enrol Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/mfa/totp/enroll/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/mfa/totp-enroll';
8
+ * ```
9
+ */
10
+ export { POST } from '../../api-handlers/mfa/totp-enroll';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ready-to-Use TOTP Verify Route (2FA CHALLENGE, not enrolment)
3
+ *
4
+ * Verifies an authenticator code at LOGIN and transitions the session to
5
+ * MFA-verified. Pair with totp-enroll/totp-confirm, which only ENROL a factor
6
+ * and deliberately do not touch session 2FA state.
7
+ *
8
+ * @example
9
+ * ```typescript
10
+ * // app/api/mfa/totp/verify/route.ts
11
+ * export { POST } from '@payez/next-mvp/routes/mfa/totp-verify';
12
+ * ```
13
+ */
14
+ export { POST } from '../../api-handlers/mfa/totp-verify';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use Passkey List Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkeys/route.ts
7
+ * export { GET } from '@payez/next-mvp/routes/passkeys/list';
8
+ * ```
9
+ */
10
+ export { GET } from '../../api-handlers/passkeys/list';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Ready-to-Use Passkey Manage Route (rename / revoke one key)
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkeys/[id]/route.ts
7
+ * export { PATCH, DELETE } from '@payez/next-mvp/routes/passkeys/manage';
8
+ * ```
9
+ */
10
+ import { NextRequest } from 'next/server';
11
+ import { PATCH as patchOne, DELETE as deleteOne } from '../../api-handlers/passkeys/manage';
12
+
13
+ type Ctx = { params: Promise<{ id: string }> };
14
+
15
+ export async function PATCH(req: NextRequest, ctx: Ctx) {
16
+ const { id } = await ctx.params;
17
+ return patchOne(req, id);
18
+ }
19
+
20
+ export async function DELETE(req: NextRequest, ctx: Ctx) {
21
+ const { id } = await ctx.params;
22
+ return deleteOne(req, id);
23
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use Passkey Registration Options Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkeys/register/options/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/passkeys/register-options';
8
+ * ```
9
+ */
10
+ export { POST } from '../../api-handlers/passkeys/register-options';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Ready-to-Use Passkey Registration Verify Route
3
+ *
4
+ * @example
5
+ * ```typescript
6
+ * // app/api/passkeys/register/verify/route.ts
7
+ * export { POST } from '@payez/next-mvp/routes/passkeys/register-verify';
8
+ * ```
9
+ */
10
+ export { POST } from '../../api-handlers/passkeys/register-verify';
@@ -20,7 +20,15 @@ let authInitPromise: Promise<ReturnType<typeof createBetterAuthInstance>> | null
20
20
 
21
21
  export type IdpTokenResult =
22
22
  | { success: true; accessToken: string; sessionData: SessionData }
23
- | { success: false; error: 'NO_SESSION' | 'NO_TOKEN'; terminal: true };
23
+ | { success: false; error: 'NO_SESSION'; terminal: true }
24
+ // Card 187871: a Better Auth session can exist with NO idpAccessToken yet
25
+ // because 2FA is required and not yet completed - the IDP never issued the
26
+ // full token. That is a RECOVERABLE state (route to 2FA), not a dead
27
+ // session. Only collapse it to NO_TOKEN/terminal when the session itself
28
+ // already claims 2FA is done (sessionData.mfaVerified === true) and there is
29
+ // still no token - that combination is genuinely broken.
30
+ | { success: false; error: 'NO_TOKEN'; terminal: true }
31
+ | { success: false; error: 'NO_TOKEN_PENDING_2FA'; terminal: false; sessionData: SessionData };
24
32
 
25
33
  function buildSessionDataFromAuthSession(session: any): SessionData | null {
26
34
  const user = session?.user;
@@ -271,6 +279,14 @@ export async function getIdpToken(request?: Request): Promise<IdpTokenResult> {
271
279
 
272
280
  const accessToken = sessionData.idpAccessToken || (sessionData as any).accessToken;
273
281
  if (!accessToken) {
282
+ // Card 187871: a live Better Auth session with no idpAccessToken and
283
+ // mfaVerified !== true is a user mid-2FA, not a dead session - the IDP
284
+ // withholds the full token until 2FA completes (idp-fetch.ts:81/113).
285
+ // Only treat this as terminal once the session itself claims 2FA is
286
+ // already done; a token missing AFTER that is genuinely broken.
287
+ if (sessionData.mfaVerified !== true) {
288
+ return { success: false, error: 'NO_TOKEN_PENDING_2FA', terminal: false, sessionData };
289
+ }
274
290
  return { success: false, error: 'NO_TOKEN', terminal: true };
275
291
  }
276
292
 
@@ -71,8 +71,29 @@ async function tryBetterAuthSession(
71
71
  try {
72
72
  const { getRedis } = await import('../lib/redis');
73
73
  const { getAppSlug } = await import('../lib/app-slug');
74
- const baKey = `ba:${getAppSlug()}:${result.session.token}`;
75
- const baRaw = await getRedis().get(baKey);
74
+ // KEY-COMPOSER MISMATCH FIX (2026-08-02).
75
+ // better-auth writes this key as `ba:${idpConfig.clientSlug || getAppSlug()}:...`
76
+ // (better-auth.ts:99). This reader used getAppSlug() ALONE, so on any
77
+ // deployment where the IDP returns a clientSlug that differs from
78
+ // APP_SLUG the lookup missed entirely: idpTokens stayed null, and every
79
+ // caller looked unauthenticated regardless of their real session.
80
+ // Observed live: writer produced ba:idp_admin_ui_graft:<token> while this
81
+ // reader asked for ba:payez_idp_admin_web:<token>.
82
+ //
83
+ // Try the writer's composition FIRST, then the original as a fallback.
84
+ // Additive on purpose: it can only ever find MORE sessions than before,
85
+ // so no consumer whose keys already resolved can regress.
86
+ let baRaw: string | null = null;
87
+ try {
88
+ const idpCfg = await getIDPClientConfig();
89
+ const writerSlug = (idpCfg as { clientSlug?: string } | null)?.clientSlug;
90
+ if (writerSlug) {
91
+ baRaw = await getRedis().get(`ba:${writerSlug}:${result.session.token}`);
92
+ }
93
+ } catch { /* config unavailable — fall through to the legacy key */ }
94
+ if (!baRaw) {
95
+ baRaw = await getRedis().get(`ba:${getAppSlug()}:${result.session.token}`);
96
+ }
76
97
  if (baRaw) {
77
98
  const baData = JSON.parse(baRaw);
78
99
  idpTokens = baData.idpTokens;
@@ -90,7 +111,19 @@ async function tryBetterAuthSession(
90
111
  idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
91
112
  || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
92
113
  mfaVerified: idpTokens?.mfaVerified ?? false,
93
- oauthProvider: 'google',
114
+ /**
115
+ * ⚠ WAS HARDCODED `'google'` — third site of QAPert finding #3 (card
116
+ * 167892), found by sweeping rather than by report.
117
+ *
118
+ * Note the line ABOVE is already honest: mfaVerified derives from what
119
+ * was stored. This one asserted the provider anyway, so a passkey
120
+ * sign-in decoded here was relabelled a Google OAuth sign-in — the
121
+ * provenance the sign-in path deliberately records, overwritten on read.
122
+ *
123
+ * The IDP emits NO provider claim (DotNetPert), so when it was not
124
+ * stored the honest answer is UNSET, not a guess.
125
+ */
126
+ oauthProvider: idpTokens?.oauthProvider ?? undefined,
94
127
  idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId,
95
128
  merchantId: idpTokens?.merchantId,
96
129
  };