@payez/next-mvp 4.1.11 → 4.1.12

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 (91) hide show
  1. package/dist/api-handlers/auth/refresh.js +15 -38
  2. package/dist/auth/better-auth.js +12 -33
  3. package/dist/client/better-auth-client.d.ts +14 -2
  4. package/dist/lib/ensure-fresh-access-token.js +8 -42
  5. package/dist/lib/session-store.d.ts +0 -1
  6. package/dist/lib/session-store.js +29 -33
  7. package/dist/lib/startup-init.js +19 -19
  8. package/dist/lib/token-lifecycle.d.ts +0 -2
  9. package/dist/lib/token-lifecycle.js +3 -21
  10. package/package.json +4 -3
  11. package/scripts/check-internal-url-usage.sh +73 -73
  12. package/src/api/index.ts +17 -17
  13. package/src/api-handlers/account/masked-info.ts +45 -45
  14. package/src/api-handlers/account/profile.ts +80 -80
  15. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  16. package/src/api-handlers/account/recovery/send-code.ts +25 -25
  17. package/src/api-handlers/account/recovery/verify-code.ts +25 -25
  18. package/src/api-handlers/account/reset-password.ts +23 -23
  19. package/src/api-handlers/account/send-code.ts +76 -76
  20. package/src/api-handlers/account/validate-password.ts +118 -118
  21. package/src/api-handlers/admin/stats.ts +249 -249
  22. package/src/api-handlers/anon/preferences.ts +123 -123
  23. package/src/api-handlers/auth/jwks.ts +20 -20
  24. package/src/api-handlers/auth/login.ts +240 -240
  25. package/src/api-handlers/auth/refresh.ts +15 -38
  26. package/src/api-handlers/auth/validate.ts +43 -43
  27. package/src/api-handlers/session/viability.ts +142 -142
  28. package/src/auth/better-auth.ts +450 -467
  29. package/src/auth/unauthenticated-routes.ts +2 -2
  30. package/src/client/better-auth-client.ts +70 -70
  31. package/src/client/fetchWithSession.ts +21 -21
  32. package/src/client/index.ts +13 -13
  33. package/src/client/useAnonSession.ts +131 -131
  34. package/src/components/admin/VibeAdminContext.tsx +87 -87
  35. package/src/config/logging-config.ts +82 -82
  36. package/src/config/unauthenticated-routes.ts +19 -19
  37. package/src/edge/internal-api-url.ts +65 -65
  38. package/src/edge/middleware.ts +42 -42
  39. package/src/hooks/useProfile.ts +75 -75
  40. package/src/lib/anon-session.ts +213 -213
  41. package/src/lib/auth-secret.ts +137 -137
  42. package/src/lib/demo-mode.ts +17 -17
  43. package/src/lib/ensure-fresh-access-token.ts +8 -42
  44. package/src/lib/idp-client-config.ts +559 -559
  45. package/src/lib/idp-fetch.ts +126 -126
  46. package/src/lib/internal-api.ts +171 -171
  47. package/src/lib/jwt-decode-client.ts +45 -45
  48. package/src/lib/rate-limit-service.ts +9 -9
  49. package/src/lib/redis.ts +26 -26
  50. package/src/lib/refresh-token-validator.ts +64 -64
  51. package/src/lib/secret-validation.ts +8 -8
  52. package/src/lib/session-store.ts +692 -695
  53. package/src/lib/session.ts +34 -34
  54. package/src/lib/startup-init.ts +251 -251
  55. package/src/lib/token-expiry.ts +40 -40
  56. package/src/lib/token-lifecycle.ts +470 -497
  57. package/src/lib/types/api-responses.ts +336 -336
  58. package/src/middleware/auth-decision.ts +43 -43
  59. package/src/models/DecodedAccessToken.ts +16 -16
  60. package/src/models/SessionModel.ts +264 -264
  61. package/src/pages/admin-roles/index.ts +9 -9
  62. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  63. package/src/routes/account/index.ts +31 -31
  64. package/src/routes/account/masked-info.ts +36 -36
  65. package/src/routes/account/send-code.ts +39 -39
  66. package/src/routes/account/verify-email.ts +40 -40
  67. package/src/routes/account/verify-sms.ts +40 -40
  68. package/src/routes/auth/index.ts +22 -22
  69. package/src/routes/auth/refresh.ts +48 -48
  70. package/src/routes/auth/session.ts +166 -166
  71. package/src/routes/index.ts +17 -17
  72. package/src/routes/session/index.ts +7 -7
  73. package/src/routes/session/refresh-viability.ts +17 -17
  74. package/src/server/auth.ts +373 -373
  75. package/src/server/decode-session.ts +202 -202
  76. package/src/services/signalrActivityService.ts +258 -258
  77. package/src/theme/ThemeProvider.tsx +39 -39
  78. package/src/theme/default.ts +33 -33
  79. package/src/theme/index.ts +31 -31
  80. package/src/theme/types.ts +69 -69
  81. package/src/theme/useTheme.ts +57 -57
  82. package/src/theme/utils.ts +40 -40
  83. package/src/types/api.ts +12 -12
  84. package/src/types/auth.d.ts +23 -23
  85. package/src/types/auth.ts +22 -22
  86. package/src/types/logging.ts +11 -11
  87. package/src/types/recovery.ts +54 -54
  88. package/src/utils/api.ts +353 -353
  89. package/src/utils/circuitBreaker.ts +40 -40
  90. package/src/utils/error-message.ts +108 -108
  91. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,213 +1,213 @@
1
- /**
2
- * Anonymous Session Store for `@payez/next-mvp`
3
- *
4
- * Provides Redis-backed anonymous sessions for tracking user preferences
5
- * before they log in. When a user logs in, their anonymous session can be
6
- * merged into their authenticated session.
7
- *
8
- * Key features:
9
- * - Generates a unique visitor ID on first visit
10
- * - Stores preferences (theme, locale, etc.) in Redis
11
- * - Tracks usage metrics (resume count for free tier, etc.)
12
- * - Provides merge functionality when user authenticates
13
- */
14
-
15
- import redis from './redis';
16
- import { randomBytes } from 'crypto';
17
- import { getAnonPrefix, getAnonCookieName } from './app-slug';
18
-
19
- // Use app-slug prefixes for multi-app isolation
20
- const getAnonKey = (id: string) => `${getAnonPrefix()}${id}`;
21
-
22
- const ANON_SESSION_TTL = 90 * 24 * 60 * 60; // 90 days in seconds
23
-
24
- // Export dynamic cookie name getter for external use
25
- export const ANON_COOKIE_NAME = getAnonCookieName();
26
-
27
- export interface AnonSessionPreferences {
28
- theme?: string;
29
- locale?: string;
30
- [key: string]: any;
31
- }
32
-
33
- export interface AnonSessionMetrics {
34
- resumeGenerationCount?: number;
35
- firstVisit?: number;
36
- lastVisit?: number;
37
- visitCount?: number;
38
- [key: string]: any;
39
- }
40
-
41
- export interface AnonSessionData {
42
- id: string;
43
- createdAt: number;
44
- updatedAt: number;
45
- preferences: AnonSessionPreferences;
46
- metrics: AnonSessionMetrics;
47
- }
48
-
49
- /**
50
- * Generates a new anonymous session ID
51
- */
52
- export function generateAnonId(): string {
53
- return randomBytes(16).toString('hex');
54
- }
55
-
56
- // getAnonKey is defined above using app-slug prefix
57
-
58
- /**
59
- * Creates a new anonymous session in Redis
60
- */
61
- export async function createAnonSession(anonId?: string): Promise<AnonSessionData> {
62
- const id = anonId || generateAnonId();
63
- const now = Date.now();
64
-
65
- const session: AnonSessionData = {
66
- id,
67
- createdAt: now,
68
- updatedAt: now,
69
- preferences: {},
70
- metrics: {
71
- firstVisit: now,
72
- lastVisit: now,
73
- visitCount: 1,
74
- },
75
- };
76
-
77
- const key = getAnonKey(id);
78
- await redis.setex(key, ANON_SESSION_TTL, JSON.stringify(session));
79
-
80
- return session;
81
- }
82
-
83
- /**
84
- * Retrieves an anonymous session from Redis
85
- */
86
- export async function getAnonSession(anonId: string): Promise<AnonSessionData | null> {
87
- if (!anonId) return null;
88
-
89
- const key = getAnonKey(anonId);
90
- const json = await redis.get(key);
91
-
92
- if (!json) return null;
93
-
94
- try {
95
- return JSON.parse(json) as AnonSessionData;
96
- } catch {
97
- return null;
98
- }
99
- }
100
-
101
- /**
102
- * Gets or creates an anonymous session
103
- */
104
- export async function getOrCreateAnonSession(anonId?: string): Promise<AnonSessionData> {
105
- if (anonId) {
106
- const existing = await getAnonSession(anonId);
107
- if (existing) {
108
- // Update last visit
109
- existing.updatedAt = Date.now();
110
- existing.metrics.lastVisit = Date.now();
111
- existing.metrics.visitCount = (existing.metrics.visitCount || 0) + 1;
112
- await saveAnonSession(existing);
113
- return existing;
114
- }
115
- }
116
-
117
- // Create new session
118
- return createAnonSession(anonId);
119
- }
120
-
121
- /**
122
- * Saves an anonymous session to Redis
123
- */
124
- export async function saveAnonSession(session: AnonSessionData): Promise<void> {
125
- session.updatedAt = Date.now();
126
- const key = getAnonKey(session.id);
127
- await redis.setex(key, ANON_SESSION_TTL, JSON.stringify(session));
128
- }
129
-
130
- /**
131
- * Updates preferences in an anonymous session
132
- */
133
- export async function updateAnonPreferences(
134
- anonId: string,
135
- preferences: Partial<AnonSessionPreferences>
136
- ): Promise<AnonSessionData | null> {
137
- const session = await getAnonSession(anonId);
138
- if (!session) return null;
139
-
140
- session.preferences = { ...session.preferences, ...preferences };
141
- await saveAnonSession(session);
142
-
143
- return session;
144
- }
145
-
146
- /**
147
- * Updates metrics in an anonymous session
148
- */
149
- export async function updateAnonMetrics(
150
- anonId: string,
151
- metrics: Partial<AnonSessionMetrics>
152
- ): Promise<AnonSessionData | null> {
153
- const session = await getAnonSession(anonId);
154
- if (!session) return null;
155
-
156
- session.metrics = { ...session.metrics, ...metrics };
157
- await saveAnonSession(session);
158
-
159
- return session;
160
- }
161
-
162
- /**
163
- * Increments a numeric metric
164
- */
165
- export async function incrementAnonMetric(
166
- anonId: string,
167
- metricName: string,
168
- amount: number = 1
169
- ): Promise<number> {
170
- const session = await getAnonSession(anonId);
171
- if (!session) return 0;
172
-
173
- const currentValue = (session.metrics[metricName] as number) || 0;
174
- const newValue = currentValue + amount;
175
- session.metrics[metricName] = newValue;
176
- await saveAnonSession(session);
177
-
178
- return newValue;
179
- }
180
-
181
- /**
182
- * Deletes an anonymous session
183
- */
184
- export async function deleteAnonSession(anonId: string): Promise<void> {
185
- if (!anonId) return;
186
- const key = getAnonKey(anonId);
187
- await redis.del(key);
188
- }
189
-
190
- /**
191
- * Merges anonymous session data into user profile data
192
- * Call this when a user logs in to preserve their pre-login preferences
193
- */
194
- export async function mergeAnonSessionToUser(
195
- anonId: string,
196
- userId: string,
197
- mergeCallback?: (anonData: AnonSessionData, userId: string) => Promise<void>
198
- ): Promise<AnonSessionData | null> {
199
- const anonSession = await getAnonSession(anonId);
200
- if (!anonSession) return null;
201
-
202
- // If a merge callback is provided, use it to persist data to user profile
203
- if (mergeCallback) {
204
- await mergeCallback(anonSession, userId);
205
- }
206
-
207
- // Optionally delete the anonymous session after merge
208
- // await deleteAnonSession(anonId);
209
-
210
- return anonSession;
211
- }
212
-
213
- // ANON_COOKIE_NAME is already exported at the top of the file
1
+ /**
2
+ * Anonymous Session Store for `@payez/next-mvp`
3
+ *
4
+ * Provides Redis-backed anonymous sessions for tracking user preferences
5
+ * before they log in. When a user logs in, their anonymous session can be
6
+ * merged into their authenticated session.
7
+ *
8
+ * Key features:
9
+ * - Generates a unique visitor ID on first visit
10
+ * - Stores preferences (theme, locale, etc.) in Redis
11
+ * - Tracks usage metrics (resume count for free tier, etc.)
12
+ * - Provides merge functionality when user authenticates
13
+ */
14
+
15
+ import redis from './redis';
16
+ import { randomBytes } from 'crypto';
17
+ import { getAnonPrefix, getAnonCookieName } from './app-slug';
18
+
19
+ // Use app-slug prefixes for multi-app isolation
20
+ const getAnonKey = (id: string) => `${getAnonPrefix()}${id}`;
21
+
22
+ const ANON_SESSION_TTL = 90 * 24 * 60 * 60; // 90 days in seconds
23
+
24
+ // Export dynamic cookie name getter for external use
25
+ export const ANON_COOKIE_NAME = getAnonCookieName();
26
+
27
+ export interface AnonSessionPreferences {
28
+ theme?: string;
29
+ locale?: string;
30
+ [key: string]: any;
31
+ }
32
+
33
+ export interface AnonSessionMetrics {
34
+ resumeGenerationCount?: number;
35
+ firstVisit?: number;
36
+ lastVisit?: number;
37
+ visitCount?: number;
38
+ [key: string]: any;
39
+ }
40
+
41
+ export interface AnonSessionData {
42
+ id: string;
43
+ createdAt: number;
44
+ updatedAt: number;
45
+ preferences: AnonSessionPreferences;
46
+ metrics: AnonSessionMetrics;
47
+ }
48
+
49
+ /**
50
+ * Generates a new anonymous session ID
51
+ */
52
+ export function generateAnonId(): string {
53
+ return randomBytes(16).toString('hex');
54
+ }
55
+
56
+ // getAnonKey is defined above using app-slug prefix
57
+
58
+ /**
59
+ * Creates a new anonymous session in Redis
60
+ */
61
+ export async function createAnonSession(anonId?: string): Promise<AnonSessionData> {
62
+ const id = anonId || generateAnonId();
63
+ const now = Date.now();
64
+
65
+ const session: AnonSessionData = {
66
+ id,
67
+ createdAt: now,
68
+ updatedAt: now,
69
+ preferences: {},
70
+ metrics: {
71
+ firstVisit: now,
72
+ lastVisit: now,
73
+ visitCount: 1,
74
+ },
75
+ };
76
+
77
+ const key = getAnonKey(id);
78
+ await redis.setex(key, ANON_SESSION_TTL, JSON.stringify(session));
79
+
80
+ return session;
81
+ }
82
+
83
+ /**
84
+ * Retrieves an anonymous session from Redis
85
+ */
86
+ export async function getAnonSession(anonId: string): Promise<AnonSessionData | null> {
87
+ if (!anonId) return null;
88
+
89
+ const key = getAnonKey(anonId);
90
+ const json = await redis.get(key);
91
+
92
+ if (!json) return null;
93
+
94
+ try {
95
+ return JSON.parse(json) as AnonSessionData;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Gets or creates an anonymous session
103
+ */
104
+ export async function getOrCreateAnonSession(anonId?: string): Promise<AnonSessionData> {
105
+ if (anonId) {
106
+ const existing = await getAnonSession(anonId);
107
+ if (existing) {
108
+ // Update last visit
109
+ existing.updatedAt = Date.now();
110
+ existing.metrics.lastVisit = Date.now();
111
+ existing.metrics.visitCount = (existing.metrics.visitCount || 0) + 1;
112
+ await saveAnonSession(existing);
113
+ return existing;
114
+ }
115
+ }
116
+
117
+ // Create new session
118
+ return createAnonSession(anonId);
119
+ }
120
+
121
+ /**
122
+ * Saves an anonymous session to Redis
123
+ */
124
+ export async function saveAnonSession(session: AnonSessionData): Promise<void> {
125
+ session.updatedAt = Date.now();
126
+ const key = getAnonKey(session.id);
127
+ await redis.setex(key, ANON_SESSION_TTL, JSON.stringify(session));
128
+ }
129
+
130
+ /**
131
+ * Updates preferences in an anonymous session
132
+ */
133
+ export async function updateAnonPreferences(
134
+ anonId: string,
135
+ preferences: Partial<AnonSessionPreferences>
136
+ ): Promise<AnonSessionData | null> {
137
+ const session = await getAnonSession(anonId);
138
+ if (!session) return null;
139
+
140
+ session.preferences = { ...session.preferences, ...preferences };
141
+ await saveAnonSession(session);
142
+
143
+ return session;
144
+ }
145
+
146
+ /**
147
+ * Updates metrics in an anonymous session
148
+ */
149
+ export async function updateAnonMetrics(
150
+ anonId: string,
151
+ metrics: Partial<AnonSessionMetrics>
152
+ ): Promise<AnonSessionData | null> {
153
+ const session = await getAnonSession(anonId);
154
+ if (!session) return null;
155
+
156
+ session.metrics = { ...session.metrics, ...metrics };
157
+ await saveAnonSession(session);
158
+
159
+ return session;
160
+ }
161
+
162
+ /**
163
+ * Increments a numeric metric
164
+ */
165
+ export async function incrementAnonMetric(
166
+ anonId: string,
167
+ metricName: string,
168
+ amount: number = 1
169
+ ): Promise<number> {
170
+ const session = await getAnonSession(anonId);
171
+ if (!session) return 0;
172
+
173
+ const currentValue = (session.metrics[metricName] as number) || 0;
174
+ const newValue = currentValue + amount;
175
+ session.metrics[metricName] = newValue;
176
+ await saveAnonSession(session);
177
+
178
+ return newValue;
179
+ }
180
+
181
+ /**
182
+ * Deletes an anonymous session
183
+ */
184
+ export async function deleteAnonSession(anonId: string): Promise<void> {
185
+ if (!anonId) return;
186
+ const key = getAnonKey(anonId);
187
+ await redis.del(key);
188
+ }
189
+
190
+ /**
191
+ * Merges anonymous session data into user profile data
192
+ * Call this when a user logs in to preserve their pre-login preferences
193
+ */
194
+ export async function mergeAnonSessionToUser(
195
+ anonId: string,
196
+ userId: string,
197
+ mergeCallback?: (anonData: AnonSessionData, userId: string) => Promise<void>
198
+ ): Promise<AnonSessionData | null> {
199
+ const anonSession = await getAnonSession(anonId);
200
+ if (!anonSession) return null;
201
+
202
+ // If a merge callback is provided, use it to persist data to user profile
203
+ if (mergeCallback) {
204
+ await mergeCallback(anonSession, userId);
205
+ }
206
+
207
+ // Optionally delete the anonymous session after merge
208
+ // await deleteAnonSession(anonId);
209
+
210
+ return anonSession;
211
+ }
212
+
213
+ // ANON_COOKIE_NAME is already exported at the top of the file