@payez/next-mvp 4.1.11 → 4.1.13

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 (92) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.d.ts +0 -3
  3. package/dist/auth/better-auth.js +164 -69
  4. package/dist/client/better-auth-client.d.ts +14 -2
  5. package/dist/lib/ensure-fresh-access-token.js +8 -42
  6. package/dist/lib/session-store.d.ts +0 -1
  7. package/dist/lib/session-store.js +29 -33
  8. package/dist/lib/startup-init.js +19 -19
  9. package/dist/lib/token-lifecycle.d.ts +0 -2
  10. package/dist/lib/token-lifecycle.js +3 -21
  11. package/package.json +5 -4
  12. package/scripts/check-internal-url-usage.sh +73 -73
  13. package/src/api/index.ts +17 -17
  14. package/src/api-handlers/account/masked-info.ts +45 -45
  15. package/src/api-handlers/account/profile.ts +80 -80
  16. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  17. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  18. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  19. package/src/api-handlers/account/reset-password.ts +23 -23
  20. package/src/api-handlers/account/send-code.ts +76 -76
  21. package/src/api-handlers/account/validate-password.ts +118 -118
  22. package/src/api-handlers/admin/stats.ts +249 -249
  23. package/src/api-handlers/anon/preferences.ts +123 -123
  24. package/src/api-handlers/auth/jwks.ts +20 -20
  25. package/src/api-handlers/auth/login.ts +240 -240
  26. package/src/api-handlers/auth/refresh.ts +15 -38
  27. package/src/api-handlers/auth/validate.ts +43 -43
  28. package/src/api-handlers/session/viability.ts +142 -142
  29. package/src/auth/better-auth.ts +130 -29
  30. package/src/auth/unauthenticated-routes.ts +2 -2
  31. package/src/client/better-auth-client.ts +70 -70
  32. package/src/client/fetchWithSession.ts +21 -21
  33. package/src/client/index.ts +13 -13
  34. package/src/client/useAnonSession.ts +131 -131
  35. package/src/components/admin/VibeAdminContext.tsx +87 -87
  36. package/src/config/logging-config.ts +82 -82
  37. package/src/config/unauthenticated-routes.ts +19 -19
  38. package/src/edge/internal-api-url.ts +65 -65
  39. package/src/edge/middleware.ts +42 -42
  40. package/src/hooks/useProfile.ts +75 -75
  41. package/src/lib/anon-session.ts +213 -213
  42. package/src/lib/auth-secret.ts +137 -137
  43. package/src/lib/demo-mode.ts +17 -17
  44. package/src/lib/ensure-fresh-access-token.ts +8 -42
  45. package/src/lib/idp-client-config.ts +559 -559
  46. package/src/lib/idp-fetch.ts +126 -126
  47. package/src/lib/internal-api.ts +171 -171
  48. package/src/lib/jwt-decode-client.ts +45 -45
  49. package/src/lib/rate-limit-service.ts +9 -9
  50. package/src/lib/redis.ts +26 -26
  51. package/src/lib/refresh-token-validator.ts +64 -64
  52. package/src/lib/secret-validation.ts +8 -8
  53. package/src/lib/session-store.ts +692 -695
  54. package/src/lib/session.ts +34 -34
  55. package/src/lib/startup-init.ts +251 -251
  56. package/src/lib/token-expiry.ts +40 -40
  57. package/src/lib/token-lifecycle.ts +470 -497
  58. package/src/lib/types/api-responses.ts +336 -336
  59. package/src/middleware/auth-decision.ts +43 -43
  60. package/src/models/DecodedAccessToken.ts +16 -16
  61. package/src/models/SessionModel.ts +264 -264
  62. package/src/pages/admin-roles/index.ts +9 -9
  63. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  64. package/src/routes/account/index.ts +31 -31
  65. package/src/routes/account/masked-info.ts +36 -36
  66. package/src/routes/account/send-code.ts +39 -39
  67. package/src/routes/account/verify-email.ts +40 -40
  68. package/src/routes/account/verify-sms.ts +40 -40
  69. package/src/routes/auth/index.ts +22 -22
  70. package/src/routes/auth/refresh.ts +48 -48
  71. package/src/routes/auth/session.ts +166 -166
  72. package/src/routes/index.ts +17 -17
  73. package/src/routes/session/index.ts +7 -7
  74. package/src/routes/session/refresh-viability.ts +17 -17
  75. package/src/server/auth.ts +373 -373
  76. package/src/server/decode-session.ts +202 -202
  77. package/src/services/signalrActivityService.ts +258 -258
  78. package/src/theme/ThemeProvider.tsx +39 -39
  79. package/src/theme/default.ts +33 -33
  80. package/src/theme/index.ts +31 -31
  81. package/src/theme/types.ts +69 -69
  82. package/src/theme/useTheme.ts +57 -57
  83. package/src/theme/utils.ts +40 -40
  84. package/src/types/api.ts +12 -12
  85. package/src/types/auth.d.ts +23 -23
  86. package/src/types/auth.ts +22 -22
  87. package/src/types/logging.ts +11 -11
  88. package/src/types/recovery.ts +54 -54
  89. package/src/utils/api.ts +353 -353
  90. package/src/utils/circuitBreaker.ts +40 -40
  91. package/src/utils/error-message.ts +108 -108
  92. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,202 +1,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
- 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
+ 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
+ }