@payez/next-mvp 4.1.10 → 4.1.20

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 (181) 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/mfa/backup-code-verify.d.ts +73 -0
  12. package/dist/api-handlers/mfa/backup-code-verify.js +153 -0
  13. package/dist/api-handlers/mfa/backup-codes.d.ts +7 -0
  14. package/dist/api-handlers/mfa/backup-codes.js +40 -0
  15. package/dist/api-handlers/mfa/methods.d.ts +3 -0
  16. package/dist/api-handlers/mfa/methods.js +41 -0
  17. package/dist/api-handlers/mfa/totp-confirm.d.ts +8 -0
  18. package/dist/api-handlers/mfa/totp-confirm.js +41 -0
  19. package/dist/api-handlers/mfa/totp-enroll.d.ts +8 -0
  20. package/dist/api-handlers/mfa/totp-enroll.js +41 -0
  21. package/dist/api-handlers/mfa/totp-verify.d.ts +47 -0
  22. package/dist/api-handlers/mfa/totp-verify.js +134 -0
  23. package/dist/api-handlers/passkeys/list.d.ts +7 -0
  24. package/dist/api-handlers/passkeys/list.js +29 -0
  25. package/dist/api-handlers/passkeys/manage.d.ts +3 -0
  26. package/dist/api-handlers/passkeys/manage.js +54 -0
  27. package/dist/api-handlers/passkeys/register-options.d.ts +8 -0
  28. package/dist/api-handlers/passkeys/register-options.js +41 -0
  29. package/dist/api-handlers/passkeys/register-verify.d.ts +7 -0
  30. package/dist/api-handlers/passkeys/register-verify.js +40 -0
  31. package/dist/api-handlers/session/viability.js +19 -0
  32. package/dist/auth/better-auth.d.ts +68 -5
  33. package/dist/auth/better-auth.js +271 -45
  34. package/dist/auth/utils/idp-client.d.ts +13 -22
  35. package/dist/auth/utils/idp-client.js +12 -82
  36. package/dist/client/better-auth-client.d.ts +14 -2
  37. package/dist/lib/ensure-fresh-access-token.js +13 -2
  38. package/dist/lib/session-store.js +28 -21
  39. package/dist/lib/startup-init.js +19 -19
  40. package/dist/lib/token-lifecycle.d.ts +2 -0
  41. package/dist/lib/token-lifecycle.js +61 -7
  42. package/dist/models/SessionModel.d.ts +17 -0
  43. package/dist/pages/login/page.d.ts +10 -1
  44. package/dist/pages/login/page.js +69 -80
  45. package/dist/pages/verify-code/page.d.ts +10 -1
  46. package/dist/pages/verify-code/page.js +72 -4
  47. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  48. package/dist/routes/auth/passkey-signin.js +18 -0
  49. package/dist/routes/auth/viability.d.ts +2 -0
  50. package/dist/routes/auth/viability.js +88 -0
  51. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  52. package/dist/routes/mfa/backup-code-verify.js +5 -0
  53. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  54. package/dist/routes/mfa/backup-codes.js +14 -0
  55. package/dist/routes/mfa/methods.d.ts +21 -0
  56. package/dist/routes/mfa/methods.js +10 -0
  57. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  58. package/dist/routes/mfa/totp-confirm.js +14 -0
  59. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  60. package/dist/routes/mfa/totp-enroll.js +14 -0
  61. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  62. package/dist/routes/mfa/totp-verify.js +18 -0
  63. package/dist/routes/passkeys/list.d.ts +10 -0
  64. package/dist/routes/passkeys/list.js +14 -0
  65. package/dist/routes/passkeys/manage.d.ts +18 -0
  66. package/dist/routes/passkeys/manage.js +13 -0
  67. package/dist/routes/passkeys/register-options.d.ts +10 -0
  68. package/dist/routes/passkeys/register-options.js +14 -0
  69. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  70. package/dist/routes/passkeys/register-verify.js +14 -0
  71. package/dist/server/auth.d.ts +2 -2
  72. package/dist/server/decode-session.js +37 -3
  73. package/package.json +958 -893
  74. package/scripts/check-internal-url-usage.sh +73 -73
  75. package/src/api/index.ts +17 -17
  76. package/src/api-handlers/account/masked-info.ts +45 -45
  77. package/src/api-handlers/account/profile.ts +99 -80
  78. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  79. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  80. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  81. package/src/api-handlers/account/reset-password.ts +23 -23
  82. package/src/api-handlers/account/send-code.ts +76 -76
  83. package/src/api-handlers/account/validate-password.ts +118 -118
  84. package/src/api-handlers/account/verify-email.ts +15 -1
  85. package/src/api-handlers/account/verify-sms.ts +15 -1
  86. package/src/api-handlers/admin/stats.ts +249 -249
  87. package/src/api-handlers/anon/preferences.ts +123 -123
  88. package/src/api-handlers/auth/jwks.ts +20 -20
  89. package/src/api-handlers/auth/login.ts +240 -240
  90. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  91. package/src/api-handlers/auth/validate.ts +43 -43
  92. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  93. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  94. package/src/api-handlers/mfa/methods.ts +46 -0
  95. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  96. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  97. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  98. package/src/api-handlers/passkeys/list.ts +31 -0
  99. package/src/api-handlers/passkeys/manage.ts +54 -0
  100. package/src/api-handlers/passkeys/register-options.ts +42 -0
  101. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  102. package/src/api-handlers/session/viability.ts +161 -142
  103. package/src/auth/better-auth.ts +708 -455
  104. package/src/auth/unauthenticated-routes.ts +2 -2
  105. package/src/auth/utils/idp-client.ts +13 -95
  106. package/src/client/better-auth-client.ts +70 -70
  107. package/src/client/fetchWithSession.ts +21 -21
  108. package/src/client/index.ts +13 -13
  109. package/src/client/useAnonSession.ts +131 -131
  110. package/src/components/admin/VibeAdminContext.tsx +87 -87
  111. package/src/config/logging-config.ts +82 -82
  112. package/src/config/unauthenticated-routes.ts +19 -19
  113. package/src/edge/internal-api-url.ts +65 -65
  114. package/src/edge/middleware.ts +42 -42
  115. package/src/hooks/useProfile.ts +75 -75
  116. package/src/lib/anon-session.ts +213 -213
  117. package/src/lib/auth-secret.ts +137 -137
  118. package/src/lib/demo-mode.ts +17 -17
  119. package/src/lib/ensure-fresh-access-token.ts +13 -2
  120. package/src/lib/idp-client-config.ts +559 -559
  121. package/src/lib/idp-fetch.ts +126 -126
  122. package/src/lib/internal-api.ts +171 -171
  123. package/src/lib/jwt-decode-client.ts +45 -45
  124. package/src/lib/rate-limit-service.ts +9 -9
  125. package/src/lib/redis.ts +26 -26
  126. package/src/lib/refresh-token-validator.ts +64 -64
  127. package/src/lib/secret-validation.ts +8 -8
  128. package/src/lib/session-store.ts +702 -695
  129. package/src/lib/session.ts +34 -34
  130. package/src/lib/startup-init.ts +251 -251
  131. package/src/lib/token-expiry.ts +40 -40
  132. package/src/lib/token-lifecycle.ts +533 -470
  133. package/src/lib/types/api-responses.ts +336 -336
  134. package/src/middleware/auth-decision.ts +43 -43
  135. package/src/models/DecodedAccessToken.ts +16 -16
  136. package/src/models/SessionModel.ts +282 -264
  137. package/src/pages/admin-roles/index.ts +9 -9
  138. package/src/pages/login/page.tsx +123 -228
  139. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  140. package/src/pages/verify-code/page.tsx +71 -5
  141. package/src/routes/account/index.ts +31 -31
  142. package/src/routes/account/masked-info.ts +36 -36
  143. package/src/routes/account/send-code.ts +39 -39
  144. package/src/routes/account/verify-email.ts +40 -40
  145. package/src/routes/account/verify-sms.ts +40 -40
  146. package/src/routes/auth/index.ts +22 -22
  147. package/src/routes/auth/passkey-signin.ts +13 -0
  148. package/src/routes/auth/refresh.ts +48 -48
  149. package/src/routes/auth/session.ts +166 -166
  150. package/src/routes/auth/viability.ts +89 -0
  151. package/src/routes/index.ts +17 -17
  152. package/src/routes/mfa/backup-code-verify.ts +1 -0
  153. package/src/routes/mfa/backup-codes.ts +10 -0
  154. package/src/routes/mfa/methods.ts +22 -0
  155. package/src/routes/mfa/totp-confirm.ts +10 -0
  156. package/src/routes/mfa/totp-enroll.ts +10 -0
  157. package/src/routes/mfa/totp-verify.ts +14 -0
  158. package/src/routes/passkeys/list.ts +10 -0
  159. package/src/routes/passkeys/manage.ts +23 -0
  160. package/src/routes/passkeys/register-options.ts +10 -0
  161. package/src/routes/passkeys/register-verify.ts +10 -0
  162. package/src/routes/session/index.ts +7 -7
  163. package/src/routes/session/refresh-viability.ts +17 -17
  164. package/src/server/auth.ts +373 -373
  165. package/src/server/decode-session.ts +235 -202
  166. package/src/services/signalrActivityService.ts +258 -258
  167. package/src/theme/ThemeProvider.tsx +39 -39
  168. package/src/theme/default.ts +33 -33
  169. package/src/theme/index.ts +31 -31
  170. package/src/theme/types.ts +69 -69
  171. package/src/theme/useTheme.ts +57 -57
  172. package/src/theme/utils.ts +40 -40
  173. package/src/types/api.ts +12 -12
  174. package/src/types/auth.d.ts +23 -23
  175. package/src/types/auth.ts +22 -22
  176. package/src/types/logging.ts +11 -11
  177. package/src/types/recovery.ts +54 -54
  178. package/src/utils/api.ts +353 -353
  179. package/src/utils/circuitBreaker.ts +40 -40
  180. package/src/utils/error-message.ts +108 -108
  181. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,202 +1,235 @@
1
- /**
2
- * Server-Side Session Decoder
3
- *
4
- * Uses Better Auth's server-side session API to get the current session.
5
- * Falls back to legacy JWT + Redis path if Better Auth session not found.
6
- */
7
-
8
- import 'server-only';
9
- import { cookies } from 'next/headers';
10
- import { jwtVerify, type JWTPayload } from 'jose';
11
- import { getSession, type SessionData } from '../lib/session-store';
12
- import { getIDPClientConfig } from '../lib/idp-client-config';
13
- import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
14
- import { ensureInitialized } from '../lib/startup-init';
15
-
16
- export interface DecodedSession {
17
- sessionData: SessionData;
18
- jwtPayload: JWTPayload & { sessionToken?: string; redisSessionId?: string };
19
- }
20
-
21
- /**
22
- * Try Better Auth's server-side session API.
23
- * Returns a DecodedSession if Better Auth has an active session, null otherwise.
24
- */
25
- async function tryBetterAuthSession(
26
- requestCookies?: { get: (name: string) => { value: string } | undefined }
27
- ): Promise<DecodedSession | null> {
28
- try {
29
- const { getBetterAuthInstance } = await import('../auth/better-auth');
30
-
31
- let auth: any = null;
32
- try {
33
- auth = await getBetterAuthInstance();
34
- } catch {
35
- return null;
36
- }
37
-
38
- if (!auth?.api?.getSession) {
39
- return null;
40
- }
41
-
42
- // Build headers from cookies for Better Auth to read
43
- const cookieStore = requestCookies || (await cookies());
44
- const headerObj = new Headers();
45
-
46
- // Collect all cookies into a Cookie header
47
- if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
48
- const allCookies = (cookieStore as any).getAll();
49
- const cookieStr = allCookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
50
- headerObj.set('cookie', cookieStr);
51
- } else {
52
- // Fallback: read known cookie names
53
- const sessionCookieName = getSessionCookieName();
54
- const secureCookieName = getSecureSessionCookieName();
55
- const parts: string[] = [];
56
- const sc = cookieStore.get(secureCookieName);
57
- if (sc?.value) parts.push(`${secureCookieName}=${sc.value}`);
58
- const nc = cookieStore.get(sessionCookieName);
59
- if (nc?.value) parts.push(`${sessionCookieName}=${nc.value}`);
60
- if (parts.length > 0) headerObj.set('cookie', parts.join('; '));
61
- }
62
-
63
- const result = await auth.api.getSession({ headers: headerObj });
64
-
65
- if (!result?.session || !result?.user) {
66
- return null;
67
- }
68
-
69
- // Read IDP tokens from BA Redis session (stored by callback route after OAuth)
70
- let idpTokens: any = null;
71
- try {
72
- const { getRedis } = await import('../lib/redis');
73
- const { getAppSlug } = await import('../lib/app-slug');
74
- const baKey = `ba:${getAppSlug()}:${result.session.token}`;
75
- const baRaw = await getRedis().get(baKey);
76
- if (baRaw) {
77
- const baData = JSON.parse(baRaw);
78
- idpTokens = baData.idpTokens;
79
- }
80
- } catch { /* Redis unavailable */ }
81
-
82
- // Map Better Auth session + IDP tokens to SessionData
83
- const sessionData: SessionData = {
84
- userId: idpTokens?.userId || result.user.id || '',
85
- email: idpTokens?.email || result.user.email || '',
86
- name: idpTokens?.name || result.user.name || undefined,
87
- roles: idpTokens?.roles || [],
88
- idpAccessToken: idpTokens?.idpAccessToken,
89
- idpRefreshToken: idpTokens?.idpRefreshToken,
90
- idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
91
- || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
92
- mfaVerified: idpTokens?.mfaVerified ?? false,
93
- oauthProvider: 'google',
94
- idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId,
95
- merchantId: idpTokens?.merchantId,
96
- };
97
-
98
- // Backwards compat: session.user.email works alongside session.email
99
- (sessionData as any).user = {
100
- id: sessionData.userId,
101
- email: sessionData.email,
102
- name: sessionData.name,
103
- roles: sessionData.roles,
104
- image: result.user.image,
105
- oauthProvider: sessionData.oauthProvider,
106
- };
107
-
108
- const jwtPayload: DecodedSession['jwtPayload'] = {
109
- sub: result.user.id,
110
- email: result.user.email,
111
- name: result.user.name,
112
- iat: Math.floor(Date.now() / 1000),
113
- exp: sessionData.idpAccessTokenExpires / 1000,
114
- sessionToken: result.session.token,
115
- };
116
-
117
- return { sessionData, jwtPayload };
118
- } catch (error) {
119
- console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
120
- return null;
121
- }
122
- }
123
-
124
- /**
125
- * Decode the session from cookies.
126
- * Tries Better Auth first, falls back to legacy JWT + Redis.
127
- *
128
- * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
129
- * If omitted, uses next/headers cookies() for server components.
130
- */
131
- export async function decodeSession(
132
- requestCookies?: { get: (name: string) => { value: string } | undefined }
133
- ): Promise<DecodedSession | null> {
134
- try {
135
- await ensureInitialized();
136
-
137
- // Try Better Auth session first
138
- const betterAuthSession = await tryBetterAuthSession(requestCookies);
139
- if (betterAuthSession) {
140
- return betterAuthSession;
141
- }
142
-
143
- // Fall back to legacy JWT + Redis path
144
- const cookieStore = requestCookies || (await cookies());
145
- const sessionCookieName = getSessionCookieName();
146
- const secureCookieName = getSecureSessionCookieName();
147
-
148
- const cookieValue =
149
- cookieStore.get(secureCookieName)?.value ||
150
- cookieStore.get(sessionCookieName)?.value;
151
-
152
- if (!cookieValue) {
153
- return null;
154
- }
155
-
156
- const config = await getIDPClientConfig();
157
- const secret = config.authSecret;
158
- if (!secret) {
159
- console.error('[DECODE-SESSION] No authSecret available from IDP config');
160
- return null;
161
- }
162
-
163
- const secretKey = new TextEncoder().encode(secret);
164
- let payload: JWTPayload;
165
- try {
166
- const result = await jwtVerify(cookieValue, secretKey);
167
- payload = result.payload;
168
- } catch (jwtError) {
169
- console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
170
- return null;
171
- }
172
-
173
- const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
174
- if (!sessionToken) {
175
- console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
176
- return null;
177
- }
178
-
179
- const sessionData = await getSession(sessionToken);
180
- if (!sessionData) {
181
- return null;
182
- }
183
-
184
- // Backwards compat: session.user.email works alongside session.email
185
- if (!(sessionData as any).user) {
186
- (sessionData as any).user = {
187
- id: sessionData.userId,
188
- email: sessionData.email,
189
- name: sessionData.name,
190
- roles: sessionData.roles,
191
- };
192
- }
193
-
194
- return {
195
- sessionData,
196
- jwtPayload: payload as DecodedSession['jwtPayload'],
197
- };
198
- } catch (error) {
199
- console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
200
- return null;
201
- }
202
- }
1
+ /**
2
+ * Server-Side Session Decoder
3
+ *
4
+ * Uses Better Auth's server-side session API to get the current session.
5
+ * Falls back to legacy JWT + Redis path if Better Auth session not found.
6
+ */
7
+
8
+ import 'server-only';
9
+ import { cookies } from 'next/headers';
10
+ import { jwtVerify, type JWTPayload } from 'jose';
11
+ import { getSession, type SessionData } from '../lib/session-store';
12
+ import { getIDPClientConfig } from '../lib/idp-client-config';
13
+ import { getSessionCookieName, getSecureSessionCookieName } from '../lib/app-slug';
14
+ import { ensureInitialized } from '../lib/startup-init';
15
+
16
+ export interface DecodedSession {
17
+ sessionData: SessionData;
18
+ jwtPayload: JWTPayload & { sessionToken?: string; redisSessionId?: string };
19
+ }
20
+
21
+ /**
22
+ * Try Better Auth's server-side session API.
23
+ * Returns a DecodedSession if Better Auth has an active session, null otherwise.
24
+ */
25
+ async function tryBetterAuthSession(
26
+ requestCookies?: { get: (name: string) => { value: string } | undefined }
27
+ ): Promise<DecodedSession | null> {
28
+ try {
29
+ const { getBetterAuthInstance } = await import('../auth/better-auth');
30
+
31
+ let auth: any = null;
32
+ try {
33
+ auth = await getBetterAuthInstance();
34
+ } catch {
35
+ return null;
36
+ }
37
+
38
+ if (!auth?.api?.getSession) {
39
+ return null;
40
+ }
41
+
42
+ // Build headers from cookies for Better Auth to read
43
+ const cookieStore = requestCookies || (await cookies());
44
+ const headerObj = new Headers();
45
+
46
+ // Collect all cookies into a Cookie header
47
+ if ('getAll' in cookieStore && typeof cookieStore.getAll === 'function') {
48
+ const allCookies = (cookieStore as any).getAll();
49
+ const cookieStr = allCookies.map((c: any) => `${c.name}=${c.value}`).join('; ');
50
+ headerObj.set('cookie', cookieStr);
51
+ } else {
52
+ // Fallback: read known cookie names
53
+ const sessionCookieName = getSessionCookieName();
54
+ const secureCookieName = getSecureSessionCookieName();
55
+ const parts: string[] = [];
56
+ const sc = cookieStore.get(secureCookieName);
57
+ if (sc?.value) parts.push(`${secureCookieName}=${sc.value}`);
58
+ const nc = cookieStore.get(sessionCookieName);
59
+ if (nc?.value) parts.push(`${sessionCookieName}=${nc.value}`);
60
+ if (parts.length > 0) headerObj.set('cookie', parts.join('; '));
61
+ }
62
+
63
+ const result = await auth.api.getSession({ headers: headerObj });
64
+
65
+ if (!result?.session || !result?.user) {
66
+ return null;
67
+ }
68
+
69
+ // Read IDP tokens from BA Redis session (stored by callback route after OAuth)
70
+ let idpTokens: any = null;
71
+ try {
72
+ const { getRedis } = await import('../lib/redis');
73
+ const { getAppSlug } = await import('../lib/app-slug');
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
+ }
97
+ if (baRaw) {
98
+ const baData = JSON.parse(baRaw);
99
+ idpTokens = baData.idpTokens;
100
+ }
101
+ } catch { /* Redis unavailable */ }
102
+
103
+ // Map Better Auth session + IDP tokens to SessionData
104
+ const sessionData: SessionData = {
105
+ userId: idpTokens?.userId || result.user.id || '',
106
+ email: idpTokens?.email || result.user.email || '',
107
+ name: idpTokens?.name || result.user.name || undefined,
108
+ roles: idpTokens?.roles || [],
109
+ idpAccessToken: idpTokens?.idpAccessToken,
110
+ idpRefreshToken: idpTokens?.idpRefreshToken,
111
+ idpAccessTokenExpires: idpTokens?.idpAccessTokenExpires
112
+ || (result.session.expiresAt ? new Date(result.session.expiresAt).getTime() : Date.now() + 24 * 60 * 60 * 1000),
113
+ mfaVerified: idpTokens?.mfaVerified ?? false,
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,
127
+ idpClientId: idpTokens?.idpClientId ?? idpTokens?.clientId,
128
+ merchantId: idpTokens?.merchantId,
129
+ };
130
+
131
+ // Backwards compat: session.user.email works alongside session.email
132
+ (sessionData as any).user = {
133
+ id: sessionData.userId,
134
+ email: sessionData.email,
135
+ name: sessionData.name,
136
+ roles: sessionData.roles,
137
+ image: result.user.image,
138
+ oauthProvider: sessionData.oauthProvider,
139
+ };
140
+
141
+ const jwtPayload: DecodedSession['jwtPayload'] = {
142
+ sub: result.user.id,
143
+ email: result.user.email,
144
+ name: result.user.name,
145
+ iat: Math.floor(Date.now() / 1000),
146
+ exp: sessionData.idpAccessTokenExpires / 1000,
147
+ sessionToken: result.session.token,
148
+ };
149
+
150
+ return { sessionData, jwtPayload };
151
+ } catch (error) {
152
+ console.warn('[DECODE-SESSION] Better Auth session check failed:', error instanceof Error ? error.message : String(error));
153
+ return null;
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Decode the session from cookies.
159
+ * Tries Better Auth first, falls back to legacy JWT + Redis.
160
+ *
161
+ * @param requestCookies Optional cookie getter for API route context (NextRequest.cookies).
162
+ * If omitted, uses next/headers cookies() for server components.
163
+ */
164
+ export async function decodeSession(
165
+ requestCookies?: { get: (name: string) => { value: string } | undefined }
166
+ ): Promise<DecodedSession | null> {
167
+ try {
168
+ await ensureInitialized();
169
+
170
+ // Try Better Auth session first
171
+ const betterAuthSession = await tryBetterAuthSession(requestCookies);
172
+ if (betterAuthSession) {
173
+ return betterAuthSession;
174
+ }
175
+
176
+ // Fall back to legacy JWT + Redis path
177
+ const cookieStore = requestCookies || (await cookies());
178
+ const sessionCookieName = getSessionCookieName();
179
+ const secureCookieName = getSecureSessionCookieName();
180
+
181
+ const cookieValue =
182
+ cookieStore.get(secureCookieName)?.value ||
183
+ cookieStore.get(sessionCookieName)?.value;
184
+
185
+ if (!cookieValue) {
186
+ return null;
187
+ }
188
+
189
+ const config = await getIDPClientConfig();
190
+ const secret = config.authSecret;
191
+ if (!secret) {
192
+ console.error('[DECODE-SESSION] No authSecret available from IDP config');
193
+ return null;
194
+ }
195
+
196
+ const secretKey = new TextEncoder().encode(secret);
197
+ let payload: JWTPayload;
198
+ try {
199
+ const result = await jwtVerify(cookieValue, secretKey);
200
+ payload = result.payload;
201
+ } catch (jwtError) {
202
+ console.warn('[DECODE-SESSION] JWT verification failed:', jwtError instanceof Error ? jwtError.message : String(jwtError));
203
+ return null;
204
+ }
205
+
206
+ const sessionToken = (payload as any).sessionToken || (payload as any).redisSessionId;
207
+ if (!sessionToken) {
208
+ console.warn('[DECODE-SESSION] JWT payload missing sessionToken/redisSessionId');
209
+ return null;
210
+ }
211
+
212
+ const sessionData = await getSession(sessionToken);
213
+ if (!sessionData) {
214
+ return null;
215
+ }
216
+
217
+ // Backwards compat: session.user.email works alongside session.email
218
+ if (!(sessionData as any).user) {
219
+ (sessionData as any).user = {
220
+ id: sessionData.userId,
221
+ email: sessionData.email,
222
+ name: sessionData.name,
223
+ roles: sessionData.roles,
224
+ };
225
+ }
226
+
227
+ return {
228
+ sessionData,
229
+ jwtPayload: payload as DecodedSession['jwtPayload'],
230
+ };
231
+ } catch (error) {
232
+ console.error('[DECODE-SESSION] Error:', error instanceof Error ? error.message : String(error));
233
+ return null;
234
+ }
235
+ }