@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,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