@payez/next-mvp 4.1.11 → 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 (178) 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 +256 -42
  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/session-store.js +28 -21
  38. package/dist/lib/startup-init.js +19 -19
  39. package/dist/lib/token-lifecycle.js +40 -4
  40. package/dist/models/SessionModel.d.ts +17 -0
  41. package/dist/pages/login/page.d.ts +10 -1
  42. package/dist/pages/login/page.js +69 -80
  43. package/dist/pages/verify-code/page.d.ts +10 -1
  44. package/dist/pages/verify-code/page.js +72 -4
  45. package/dist/routes/auth/passkey-signin.d.ts +13 -0
  46. package/dist/routes/auth/passkey-signin.js +18 -0
  47. package/dist/routes/auth/viability.d.ts +2 -0
  48. package/dist/routes/auth/viability.js +88 -0
  49. package/dist/routes/mfa/backup-code-verify.d.ts +1 -0
  50. package/dist/routes/mfa/backup-code-verify.js +5 -0
  51. package/dist/routes/mfa/backup-codes.d.ts +10 -0
  52. package/dist/routes/mfa/backup-codes.js +14 -0
  53. package/dist/routes/mfa/methods.d.ts +21 -0
  54. package/dist/routes/mfa/methods.js +10 -0
  55. package/dist/routes/mfa/totp-confirm.d.ts +10 -0
  56. package/dist/routes/mfa/totp-confirm.js +14 -0
  57. package/dist/routes/mfa/totp-enroll.d.ts +10 -0
  58. package/dist/routes/mfa/totp-enroll.js +14 -0
  59. package/dist/routes/mfa/totp-verify.d.ts +14 -0
  60. package/dist/routes/mfa/totp-verify.js +18 -0
  61. package/dist/routes/passkeys/list.d.ts +10 -0
  62. package/dist/routes/passkeys/list.js +14 -0
  63. package/dist/routes/passkeys/manage.d.ts +18 -0
  64. package/dist/routes/passkeys/manage.js +13 -0
  65. package/dist/routes/passkeys/register-options.d.ts +10 -0
  66. package/dist/routes/passkeys/register-options.js +14 -0
  67. package/dist/routes/passkeys/register-verify.d.ts +10 -0
  68. package/dist/routes/passkeys/register-verify.js +14 -0
  69. package/dist/server/auth.d.ts +2 -2
  70. package/dist/server/decode-session.js +37 -3
  71. package/package.json +958 -893
  72. package/scripts/check-internal-url-usage.sh +73 -73
  73. package/src/api/index.ts +17 -17
  74. package/src/api-handlers/account/masked-info.ts +45 -45
  75. package/src/api-handlers/account/profile.ts +99 -80
  76. package/src/api-handlers/account/recovery/initiate.ts +23 -23
  77. package/src/api-handlers/account/recovery/send-code.ts +73 -25
  78. package/src/api-handlers/account/recovery/verify-code.ts +73 -25
  79. package/src/api-handlers/account/reset-password.ts +23 -23
  80. package/src/api-handlers/account/send-code.ts +76 -76
  81. package/src/api-handlers/account/validate-password.ts +118 -118
  82. package/src/api-handlers/account/verify-email.ts +15 -1
  83. package/src/api-handlers/account/verify-sms.ts +15 -1
  84. package/src/api-handlers/admin/stats.ts +249 -249
  85. package/src/api-handlers/anon/preferences.ts +123 -123
  86. package/src/api-handlers/auth/jwks.ts +20 -20
  87. package/src/api-handlers/auth/login.ts +240 -240
  88. package/src/api-handlers/auth/passkey-signin.ts +98 -0
  89. package/src/api-handlers/auth/validate.ts +43 -43
  90. package/src/api-handlers/mfa/backup-code-verify.ts +175 -0
  91. package/src/api-handlers/mfa/backup-codes.ts +41 -0
  92. package/src/api-handlers/mfa/methods.ts +46 -0
  93. package/src/api-handlers/mfa/totp-confirm.ts +42 -0
  94. package/src/api-handlers/mfa/totp-enroll.ts +42 -0
  95. package/src/api-handlers/mfa/totp-verify.ts +156 -0
  96. package/src/api-handlers/passkeys/list.ts +31 -0
  97. package/src/api-handlers/passkeys/manage.ts +54 -0
  98. package/src/api-handlers/passkeys/register-options.ts +42 -0
  99. package/src/api-handlers/passkeys/register-verify.ts +41 -0
  100. package/src/api-handlers/session/viability.ts +161 -142
  101. package/src/auth/better-auth.ts +247 -6
  102. package/src/auth/unauthenticated-routes.ts +2 -2
  103. package/src/auth/utils/idp-client.ts +13 -95
  104. package/src/client/better-auth-client.ts +70 -70
  105. package/src/client/fetchWithSession.ts +21 -21
  106. package/src/client/index.ts +13 -13
  107. package/src/client/useAnonSession.ts +131 -131
  108. package/src/components/admin/VibeAdminContext.tsx +87 -87
  109. package/src/config/logging-config.ts +82 -82
  110. package/src/config/unauthenticated-routes.ts +19 -19
  111. package/src/edge/internal-api-url.ts +65 -65
  112. package/src/edge/middleware.ts +42 -42
  113. package/src/hooks/useProfile.ts +75 -75
  114. package/src/lib/anon-session.ts +213 -213
  115. package/src/lib/auth-secret.ts +137 -137
  116. package/src/lib/demo-mode.ts +17 -17
  117. package/src/lib/idp-client-config.ts +559 -559
  118. package/src/lib/idp-fetch.ts +126 -126
  119. package/src/lib/internal-api.ts +171 -171
  120. package/src/lib/jwt-decode-client.ts +45 -45
  121. package/src/lib/rate-limit-service.ts +9 -9
  122. package/src/lib/redis.ts +26 -26
  123. package/src/lib/refresh-token-validator.ts +64 -64
  124. package/src/lib/secret-validation.ts +8 -8
  125. package/src/lib/session-store.ts +702 -695
  126. package/src/lib/session.ts +34 -34
  127. package/src/lib/startup-init.ts +251 -251
  128. package/src/lib/token-expiry.ts +40 -40
  129. package/src/lib/token-lifecycle.ts +533 -497
  130. package/src/lib/types/api-responses.ts +336 -336
  131. package/src/middleware/auth-decision.ts +43 -43
  132. package/src/models/DecodedAccessToken.ts +16 -16
  133. package/src/models/SessionModel.ts +282 -264
  134. package/src/pages/admin-roles/index.ts +9 -9
  135. package/src/pages/login/page.tsx +123 -228
  136. package/src/pages/security/EnhancedSecurityPage.tsx +9 -9
  137. package/src/pages/verify-code/page.tsx +71 -5
  138. package/src/routes/account/index.ts +31 -31
  139. package/src/routes/account/masked-info.ts +36 -36
  140. package/src/routes/account/send-code.ts +39 -39
  141. package/src/routes/account/verify-email.ts +40 -40
  142. package/src/routes/account/verify-sms.ts +40 -40
  143. package/src/routes/auth/index.ts +22 -22
  144. package/src/routes/auth/passkey-signin.ts +13 -0
  145. package/src/routes/auth/refresh.ts +48 -48
  146. package/src/routes/auth/session.ts +166 -166
  147. package/src/routes/auth/viability.ts +89 -0
  148. package/src/routes/index.ts +17 -17
  149. package/src/routes/mfa/backup-code-verify.ts +1 -0
  150. package/src/routes/mfa/backup-codes.ts +10 -0
  151. package/src/routes/mfa/methods.ts +22 -0
  152. package/src/routes/mfa/totp-confirm.ts +10 -0
  153. package/src/routes/mfa/totp-enroll.ts +10 -0
  154. package/src/routes/mfa/totp-verify.ts +14 -0
  155. package/src/routes/passkeys/list.ts +10 -0
  156. package/src/routes/passkeys/manage.ts +23 -0
  157. package/src/routes/passkeys/register-options.ts +10 -0
  158. package/src/routes/passkeys/register-verify.ts +10 -0
  159. package/src/routes/session/index.ts +7 -7
  160. package/src/routes/session/refresh-viability.ts +17 -17
  161. package/src/server/auth.ts +373 -373
  162. package/src/server/decode-session.ts +235 -202
  163. package/src/services/signalrActivityService.ts +258 -258
  164. package/src/theme/ThemeProvider.tsx +39 -39
  165. package/src/theme/default.ts +33 -33
  166. package/src/theme/index.ts +31 -31
  167. package/src/theme/types.ts +69 -69
  168. package/src/theme/useTheme.ts +57 -57
  169. package/src/theme/utils.ts +40 -40
  170. package/src/types/api.ts +12 -12
  171. package/src/types/auth.d.ts +23 -23
  172. package/src/types/auth.ts +22 -22
  173. package/src/types/logging.ts +11 -11
  174. package/src/types/recovery.ts +54 -54
  175. package/src/utils/api.ts +353 -353
  176. package/src/utils/circuitBreaker.ts +40 -40
  177. package/src/utils/error-message.ts +108 -108
  178. package/src/utils/layout/reservedSpace.ts +124 -124
@@ -1,75 +1,75 @@
1
- 'use client';
2
-
3
- /**
4
- * ⚠️ WARNING: This hook cannot be used directly from the @payez/next-mvp package!
5
- *
6
- * @deprecated Import this hook from your local app instead
7
- *
8
- * **Why?** This hook depends on useAuth, which requires SessionProvider context that cannot cross package boundaries.
9
- *
10
- * **Solution:** Create a local version in your app (e.g., src/hooks/useProfile.ts):
11
- *
12
- * @example
13
- * ```typescript
14
- * // src/hooks/useProfile.ts (YOUR APP)
15
- * 'use client';
16
- * import { useQuery } from '@tanstack/react-query';
17
- * import { useAuth } from './useAuth'; // Your local useAuth
18
- * import { isApiSuccess } from '@payez/next-mvp/lib/standardized-client-api';
19
- *
20
- * export function useProfile() {
21
- * const { isAuthenticated, apiCall } = useAuth();
22
- * return useQuery({
23
- * queryKey: ['account', 'profile'],
24
- * enabled: isAuthenticated,
25
- * queryFn: async () => {
26
- * const result = await apiCall('/api/account/profile', 'GET');
27
- * if (isApiSuccess(result)) return result.data;
28
- * throw new Error(result.message);
29
- * }
30
- * });
31
- * }
32
- * ```
33
- *
34
- * @see {@link https://github.com/payez/next-mvp/blob/main/docs/centralized-auth-api-pattern.md#usage-patterns Complete documentation}
35
- */
36
-
37
- import { useQuery } from '@tanstack/react-query';
38
- import { useAuth } from './useAuth';
39
- import { isApiSuccess, isApiError } from '../lib/standardized-client-api';
40
-
41
- export type ProfileResponse = {
42
- email: string;
43
- user_id?: string;
44
- first_name?: string;
45
- last_name?: string;
46
- full_name?: string;
47
- phone_number?: string;
48
- email_confirmed?: boolean;
49
- phone_confirmed?: boolean;
50
- two_factor_enabled?: boolean;
51
- roles?: string[];
52
- contact_information?: Record<string, any>;
53
- [key: string]: any;
54
- };
55
-
56
- export function useProfile() {
57
- const { isAuthenticated, apiCall } = useAuth();
58
-
59
- return useQuery<ProfileResponse, Error>({
60
- queryKey: ['account', 'profile'],
61
- enabled: isAuthenticated,
62
- queryFn: async () => {
63
- const result = await apiCall<ProfileResponse>('/api/account/profile', 'GET');
64
- if (isApiSuccess(result)) {
65
- return result.data;
66
- }
67
- // If API returned standardized error
68
- if (isApiError(result)) {
69
- throw new Error(result.message || 'Failed to load profile');
70
- }
71
- throw new Error('Failed to load profile');
72
- },
73
- staleTime: 5 * 60 * 1000,
74
- });
75
- }
1
+ 'use client';
2
+
3
+ /**
4
+ * ⚠️ WARNING: This hook cannot be used directly from the @payez/next-mvp package!
5
+ *
6
+ * @deprecated Import this hook from your local app instead
7
+ *
8
+ * **Why?** This hook depends on useAuth, which requires SessionProvider context that cannot cross package boundaries.
9
+ *
10
+ * **Solution:** Create a local version in your app (e.g., src/hooks/useProfile.ts):
11
+ *
12
+ * @example
13
+ * ```typescript
14
+ * // src/hooks/useProfile.ts (YOUR APP)
15
+ * 'use client';
16
+ * import { useQuery } from '@tanstack/react-query';
17
+ * import { useAuth } from './useAuth'; // Your local useAuth
18
+ * import { isApiSuccess } from '@payez/next-mvp/lib/standardized-client-api';
19
+ *
20
+ * export function useProfile() {
21
+ * const { isAuthenticated, apiCall } = useAuth();
22
+ * return useQuery({
23
+ * queryKey: ['account', 'profile'],
24
+ * enabled: isAuthenticated,
25
+ * queryFn: async () => {
26
+ * const result = await apiCall('/api/account/profile', 'GET');
27
+ * if (isApiSuccess(result)) return result.data;
28
+ * throw new Error(result.message);
29
+ * }
30
+ * });
31
+ * }
32
+ * ```
33
+ *
34
+ * @see {@link https://github.com/payez/next-mvp/blob/main/docs/centralized-auth-api-pattern.md#usage-patterns Complete documentation}
35
+ */
36
+
37
+ import { useQuery } from '@tanstack/react-query';
38
+ import { useAuth } from './useAuth';
39
+ import { isApiSuccess, isApiError } from '../lib/standardized-client-api';
40
+
41
+ export type ProfileResponse = {
42
+ email: string;
43
+ user_id?: string;
44
+ first_name?: string;
45
+ last_name?: string;
46
+ full_name?: string;
47
+ phone_number?: string;
48
+ email_confirmed?: boolean;
49
+ phone_confirmed?: boolean;
50
+ two_factor_enabled?: boolean;
51
+ roles?: string[];
52
+ contact_information?: Record<string, any>;
53
+ [key: string]: any;
54
+ };
55
+
56
+ export function useProfile() {
57
+ const { isAuthenticated, apiCall } = useAuth();
58
+
59
+ return useQuery<ProfileResponse, Error>({
60
+ queryKey: ['account', 'profile'],
61
+ enabled: isAuthenticated,
62
+ queryFn: async () => {
63
+ const result = await apiCall<ProfileResponse>('/api/account/profile', 'GET');
64
+ if (isApiSuccess(result)) {
65
+ return result.data;
66
+ }
67
+ // If API returned standardized error
68
+ if (isApiError(result)) {
69
+ throw new Error(result.message || 'Failed to load profile');
70
+ }
71
+ throw new Error('Failed to load profile');
72
+ },
73
+ staleTime: 5 * 60 * 1000,
74
+ });
75
+ }
@@ -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