hazo_auth 4.2.0 → 4.3.0

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 (82) hide show
  1. package/bin/hazo_auth.mjs +35 -0
  2. package/cli-src/assets/images/forgot_password_default.jpg +0 -0
  3. package/cli-src/assets/images/login_default.jpg +0 -0
  4. package/cli-src/assets/images/register_default.jpg +0 -0
  5. package/cli-src/assets/images/reset_password_default.jpg +0 -0
  6. package/cli-src/assets/images/verify_email_default.jpg +0 -0
  7. package/cli-src/cli/generate.ts +276 -0
  8. package/cli-src/cli/index.ts +207 -0
  9. package/cli-src/cli/init.ts +254 -0
  10. package/cli-src/cli/init_users.ts +376 -0
  11. package/cli-src/cli/validate.ts +581 -0
  12. package/cli-src/lib/already_logged_in_config.server.ts +46 -0
  13. package/cli-src/lib/app_logger.ts +24 -0
  14. package/cli-src/lib/auth/auth_cache.ts +220 -0
  15. package/cli-src/lib/auth/auth_rate_limiter.ts +121 -0
  16. package/cli-src/lib/auth/auth_types.ts +110 -0
  17. package/cli-src/lib/auth/auth_utils.server.ts +196 -0
  18. package/cli-src/lib/auth/hazo_get_auth.server.ts +512 -0
  19. package/cli-src/lib/auth/index.ts +23 -0
  20. package/cli-src/lib/auth/nextauth_config.ts +227 -0
  21. package/cli-src/lib/auth/scope_cache.ts +233 -0
  22. package/cli-src/lib/auth/server_auth.ts +88 -0
  23. package/cli-src/lib/auth/session_token_validator.edge.ts +91 -0
  24. package/cli-src/lib/auth_utility_config.server.ts +136 -0
  25. package/cli-src/lib/config/config_loader.server.ts +164 -0
  26. package/cli-src/lib/config/default_config.ts +199 -0
  27. package/cli-src/lib/email_verification_config.server.ts +63 -0
  28. package/cli-src/lib/file_types_config.server.ts +25 -0
  29. package/cli-src/lib/forgot_password_config.server.ts +63 -0
  30. package/cli-src/lib/hazo_connect_instance.server.ts +101 -0
  31. package/cli-src/lib/hazo_connect_setup.server.ts +194 -0
  32. package/cli-src/lib/hazo_connect_setup.ts +54 -0
  33. package/cli-src/lib/index.ts +46 -0
  34. package/cli-src/lib/login_config.server.ts +106 -0
  35. package/cli-src/lib/messages_config.server.ts +45 -0
  36. package/cli-src/lib/migrations/apply_migration.ts +105 -0
  37. package/cli-src/lib/my_settings_config.server.ts +135 -0
  38. package/cli-src/lib/oauth_config.server.ts +87 -0
  39. package/cli-src/lib/password_requirements_config.server.ts +40 -0
  40. package/cli-src/lib/profile_pic_menu_config.server.ts +138 -0
  41. package/cli-src/lib/profile_picture_config.server.ts +56 -0
  42. package/cli-src/lib/register_config.server.ts +101 -0
  43. package/cli-src/lib/reset_password_config.server.ts +103 -0
  44. package/cli-src/lib/scope_hierarchy_config.server.ts +151 -0
  45. package/cli-src/lib/services/email_service.ts +587 -0
  46. package/cli-src/lib/services/email_verification_service.ts +270 -0
  47. package/cli-src/lib/services/index.ts +16 -0
  48. package/cli-src/lib/services/login_service.ts +150 -0
  49. package/cli-src/lib/services/oauth_service.ts +494 -0
  50. package/cli-src/lib/services/password_change_service.ts +154 -0
  51. package/cli-src/lib/services/password_reset_service.ts +418 -0
  52. package/cli-src/lib/services/profile_picture_remove_service.ts +120 -0
  53. package/cli-src/lib/services/profile_picture_service.ts +451 -0
  54. package/cli-src/lib/services/profile_picture_source_mapper.ts +62 -0
  55. package/cli-src/lib/services/registration_service.ts +185 -0
  56. package/cli-src/lib/services/scope_labels_service.ts +348 -0
  57. package/cli-src/lib/services/scope_service.ts +778 -0
  58. package/cli-src/lib/services/session_token_service.ts +177 -0
  59. package/cli-src/lib/services/token_service.ts +240 -0
  60. package/cli-src/lib/services/user_profiles_cache.ts +189 -0
  61. package/cli-src/lib/services/user_profiles_service.ts +264 -0
  62. package/cli-src/lib/services/user_scope_service.ts +554 -0
  63. package/cli-src/lib/services/user_update_service.ts +141 -0
  64. package/cli-src/lib/ui_shell_config.server.ts +73 -0
  65. package/cli-src/lib/ui_sizes_config.server.ts +37 -0
  66. package/cli-src/lib/user_fields_config.server.ts +31 -0
  67. package/cli-src/lib/user_management_config.server.ts +39 -0
  68. package/cli-src/lib/user_profiles_config.server.ts +55 -0
  69. package/cli-src/lib/utils/api_route_helpers.ts +60 -0
  70. package/cli-src/lib/utils/error_sanitizer.ts +75 -0
  71. package/cli-src/lib/utils/password_validator.ts +65 -0
  72. package/cli-src/lib/utils.ts +11 -0
  73. package/cli-src/server/logging/logger_service.ts +56 -0
  74. package/dist/cli/index.js +18 -0
  75. package/dist/cli/init_users.d.ts +17 -0
  76. package/dist/cli/init_users.d.ts.map +1 -0
  77. package/dist/cli/init_users.js +307 -0
  78. package/dist/components/layouts/shared/config/layout_customization.d.ts +2 -7
  79. package/dist/components/layouts/shared/config/layout_customization.d.ts.map +1 -1
  80. package/dist/lib/utils/password_validator.d.ts +7 -1
  81. package/dist/lib/utils/password_validator.d.ts.map +1 -1
  82. package/package.json +5 -2
@@ -0,0 +1,177 @@
1
+ // file_description: service for creating and validating JWT session tokens for authentication
2
+ // Uses jose library for Edge-compatible JWT operations
3
+ // section: imports
4
+ import { SignJWT, jwtVerify } from "jose";
5
+ import { create_app_logger } from "../app_logger";
6
+ import { get_filename, get_line_number } from "../utils/api_route_helpers";
7
+
8
+ // section: types
9
+ export type SessionTokenPayload = {
10
+ user_id: string;
11
+ email: string;
12
+ iat: number;
13
+ exp: number;
14
+ };
15
+
16
+ export type ValidateSessionTokenResult = {
17
+ valid: boolean;
18
+ user_id?: string;
19
+ email?: string;
20
+ };
21
+
22
+ // section: helpers
23
+ /**
24
+ * Gets JWT secret from environment variables
25
+ * @returns JWT secret as Uint8Array for jose library
26
+ * @throws Error if JWT_SECRET is not set
27
+ */
28
+ function get_jwt_secret(): Uint8Array {
29
+ const jwt_secret = process.env.JWT_SECRET;
30
+
31
+ if (!jwt_secret) {
32
+ const logger = create_app_logger();
33
+ logger.error("session_token_jwt_secret_missing", {
34
+ filename: get_filename(),
35
+ line_number: get_line_number(),
36
+ error: "JWT_SECRET environment variable is required",
37
+ });
38
+ throw new Error("JWT_SECRET environment variable is required");
39
+ }
40
+
41
+ // Convert string secret to Uint8Array for jose
42
+ return new TextEncoder().encode(jwt_secret);
43
+ }
44
+
45
+ /**
46
+ * Gets session token expiry in seconds (default: 30 days)
47
+ * @returns Number of seconds until token expires
48
+ */
49
+ function get_session_token_expiry_seconds(): number {
50
+ // Default: 30 days = 30 * 24 * 60 * 60 = 2,592,000 seconds
51
+ const default_expiry_seconds = 60 * 60 * 24 * 30;
52
+
53
+ // Could be extended to read from config in the future
54
+ // For now, use default 30 days to match cookie expiry
55
+ return default_expiry_seconds;
56
+ }
57
+
58
+ // section: main_functions
59
+ /**
60
+ * Creates a JWT session token for a user
61
+ * Token includes user_id, email, issued at time, and expiration
62
+ * @param user_id - User ID
63
+ * @param email - User email address
64
+ * @returns JWT token string
65
+ */
66
+ export async function create_session_token(
67
+ user_id: string,
68
+ email: string,
69
+ ): Promise<string> {
70
+ const logger = create_app_logger();
71
+
72
+ try {
73
+ const secret = get_jwt_secret();
74
+ const now = Math.floor(Date.now() / 1000); // Current time in seconds
75
+ const expiry_seconds = get_session_token_expiry_seconds();
76
+ const exp = now + expiry_seconds;
77
+
78
+ const jwt = await new SignJWT({
79
+ user_id,
80
+ email,
81
+ })
82
+ .setProtectedHeader({ alg: "HS256" })
83
+ .setIssuedAt(now)
84
+ .setExpirationTime(exp)
85
+ .sign(secret);
86
+
87
+ logger.info("session_token_created", {
88
+ filename: get_filename(),
89
+ line_number: get_line_number(),
90
+ user_id,
91
+ email,
92
+ expires_in_seconds: expiry_seconds,
93
+ });
94
+
95
+ return jwt;
96
+ } catch (error) {
97
+ const error_message = error instanceof Error ? error.message : "Unknown error";
98
+ const error_stack = error instanceof Error ? error.stack : undefined;
99
+
100
+ logger.error("session_token_creation_failed", {
101
+ filename: get_filename(),
102
+ line_number: get_line_number(),
103
+ user_id,
104
+ email,
105
+ error_message,
106
+ error_stack,
107
+ });
108
+
109
+ throw new Error("Failed to create session token");
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Validates a JWT session token
115
+ * Checks signature and expiration
116
+ * @param token - JWT token string
117
+ * @returns Validation result with user_id and email if valid
118
+ */
119
+ export async function validate_session_token(
120
+ token: string,
121
+ ): Promise<ValidateSessionTokenResult> {
122
+ const logger = create_app_logger();
123
+
124
+ try {
125
+ const secret = get_jwt_secret();
126
+
127
+ const { payload } = await jwtVerify(token, secret, {
128
+ algorithms: ["HS256"],
129
+ });
130
+
131
+ // Extract user_id and email from payload
132
+ const user_id = payload.user_id as string;
133
+ const email = payload.email as string;
134
+
135
+ if (!user_id || !email) {
136
+ logger.warn("session_token_invalid_payload", {
137
+ filename: get_filename(),
138
+ line_number: get_line_number(),
139
+ error: "Token payload missing user_id or email",
140
+ });
141
+
142
+ return { valid: false };
143
+ }
144
+
145
+ logger.info("session_token_validated", {
146
+ filename: get_filename(),
147
+ line_number: get_line_number(),
148
+ user_id,
149
+ email,
150
+ });
151
+
152
+ return {
153
+ valid: true,
154
+ user_id,
155
+ email,
156
+ };
157
+ } catch (error) {
158
+ const error_message = error instanceof Error ? error.message : "Unknown error";
159
+
160
+ // jose throws JWTExpired, JWTInvalid, etc. - these are expected for invalid tokens
161
+ logger.debug("session_token_validation_failed", {
162
+ filename: get_filename(),
163
+ line_number: get_line_number(),
164
+ error_message,
165
+ });
166
+
167
+ return { valid: false };
168
+ }
169
+ }
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
@@ -0,0 +1,240 @@
1
+ // file_description: shared service for creating and managing tokens in hazo_refresh_tokens table
2
+ // section: imports
3
+ import type { HazoConnectAdapter } from "hazo_connect";
4
+ import { createCrudService } from "hazo_connect/server";
5
+ import { randomBytes, randomUUID } from "crypto";
6
+ import argon2 from "argon2";
7
+ import { read_config_section } from "../config/config_loader.server";
8
+ import { create_app_logger } from "../app_logger";
9
+
10
+ // section: types
11
+ export type TokenType = "refresh" | "password_reset" | "email_verification";
12
+
13
+ export type CreateTokenParams = {
14
+ adapter: HazoConnectAdapter;
15
+ user_id: string;
16
+ token_type: TokenType;
17
+ };
18
+
19
+ export type CreateTokenResult = {
20
+ success: boolean;
21
+ raw_token?: string;
22
+ error?: string;
23
+ };
24
+
25
+ // section: helpers
26
+ /**
27
+ * Gets token expiry hours from hazo_auth_config.ini for a specific token type
28
+ * Falls back to defaults if config is not found
29
+ * @param token_type - The type of token (refresh, password_reset, email_verification)
30
+ * @returns Number of hours until token expires
31
+ */
32
+ function get_token_expiry_hours(token_type: TokenType): number {
33
+ const default_expiries: Record<TokenType, number> = {
34
+ refresh: 720, // 30 days
35
+ password_reset: 0.167, // 10 minutes
36
+ email_verification: 48, // 48 hours
37
+ };
38
+
39
+ const logger = create_app_logger();
40
+ const token_config_section = read_config_section("hazo_auth__tokens");
41
+
42
+ // Get expiry from config or environment variable or default
43
+ const config_key = `${token_type}_expiry_hours`;
44
+ const env_key = `HAZO_AUTH_${token_type.toUpperCase()}_TOKEN_EXPIRY_HOURS`;
45
+
46
+ const expiry_hours =
47
+ token_config_section?.[config_key] ||
48
+ process.env[env_key] ||
49
+ default_expiries[token_type];
50
+
51
+ return parseFloat(String(expiry_hours)) || default_expiries[token_type];
52
+ }
53
+
54
+ /**
55
+ * Creates a token for a user and stores it in hazo_refresh_tokens table
56
+ * Invalidates any existing tokens of the same type for the user before creating a new one
57
+ * @param params - Token creation parameters (adapter, user_id, token_type)
58
+ * @returns Token creation result with raw_token (for sending to user) or error
59
+ */
60
+ export async function create_token(
61
+ params: CreateTokenParams,
62
+ ): Promise<CreateTokenResult> {
63
+ try {
64
+ const { adapter, user_id, token_type } = params;
65
+
66
+ // Create CRUD service for hazo_refresh_tokens table
67
+ const tokens_service = createCrudService(adapter, "hazo_refresh_tokens");
68
+
69
+ // Invalidate any existing tokens of this type for this user
70
+ // If token_type column doesn't exist, this will fail - catch and continue
71
+ let existing_tokens: unknown[] = [];
72
+ try {
73
+ existing_tokens = (await tokens_service.findBy({
74
+ user_id: user_id,
75
+ token_type: token_type,
76
+ })) as unknown[];
77
+ } catch (error) {
78
+ // If token_type column doesn't exist, try without it
79
+ // This is a fallback for databases that haven't had the migration applied
80
+ const logger = create_app_logger();
81
+ const error_message = error instanceof Error ? error.message : "Unknown error";
82
+ logger.warn("token_service_token_type_column_missing", {
83
+ filename: "token_service.ts",
84
+ line_number: 0,
85
+ user_id,
86
+ token_type,
87
+ error: error_message,
88
+ note: "token_type column may not exist, trying without filter",
89
+ });
90
+ // Try to find tokens by user_id only (less precise but works without migration)
91
+ try {
92
+ existing_tokens = (await tokens_service.findBy({
93
+ user_id: user_id,
94
+ })) as unknown[];
95
+ } catch (fallbackError) {
96
+ // If that also fails, log and continue (will just create new token)
97
+ const fallback_error_message = fallbackError instanceof Error ? fallbackError.message : "Unknown error";
98
+ logger.warn("token_service_query_existing_tokens_failed", {
99
+ filename: "token_service.ts",
100
+ line_number: 0,
101
+ user_id,
102
+ error: fallback_error_message,
103
+ note: "Could not query existing tokens, will create new token anyway",
104
+ });
105
+ }
106
+ }
107
+
108
+ if (Array.isArray(existing_tokens) && existing_tokens.length > 0) {
109
+ // Delete existing tokens (of this type if token_type exists, or all for user if not)
110
+ for (const token of existing_tokens) {
111
+ try {
112
+ await tokens_service.deleteById((token as { id: unknown }).id);
113
+ } catch (deleteError) {
114
+ const logger = create_app_logger();
115
+ const error_message = deleteError instanceof Error ? deleteError.message : "Unknown error";
116
+ logger.warn("token_service_delete_existing_token_failed", {
117
+ filename: "token_service.ts",
118
+ line_number: 0,
119
+ user_id,
120
+ token_id: (token as { id: unknown }).id,
121
+ error: error_message,
122
+ });
123
+ }
124
+ }
125
+ }
126
+
127
+ // Generate a secure random token
128
+ const raw_token = randomBytes(32).toString("hex");
129
+
130
+ // Hash the token before storing
131
+ const token_hash = await argon2.hash(raw_token);
132
+
133
+ // Get expiry hours from config
134
+ const expiry_hours = get_token_expiry_hours(token_type);
135
+
136
+ // Calculate expiration time (convert hours to milliseconds)
137
+ const expires_at = new Date();
138
+ expires_at.setTime(expires_at.getTime() + expiry_hours * 60 * 60 * 1000);
139
+
140
+ const now = new Date().toISOString();
141
+
142
+ // Insert the token into the database
143
+ // Try with token_type first, fallback to without if column doesn't exist
144
+ let inserted_tokens: unknown[];
145
+ try {
146
+ inserted_tokens = (await tokens_service.insert({
147
+ id: randomUUID(),
148
+ user_id: user_id,
149
+ token_hash: token_hash,
150
+ token_type: token_type,
151
+ expires_at: expires_at.toISOString(),
152
+ created_at: now,
153
+ })) as unknown[];
154
+ } catch (error) {
155
+ // If token_type column doesn't exist, try without it
156
+ const logger = create_app_logger();
157
+ const error_message = error instanceof Error ? error.message : "Unknown error";
158
+ logger.warn("token_service_insert_with_token_type_failed", {
159
+ filename: "token_service.ts",
160
+ line_number: 0,
161
+ user_id,
162
+ token_type,
163
+ error: error_message,
164
+ note: "token_type column may not exist, inserting without it",
165
+ });
166
+ // Fallback: insert without token_type (will use default if column exists with default)
167
+ inserted_tokens = (await tokens_service.insert({
168
+ id: randomUUID(),
169
+ user_id: user_id,
170
+ token_hash: token_hash,
171
+ expires_at: expires_at.toISOString(),
172
+ created_at: now,
173
+ })) as unknown[];
174
+ }
175
+
176
+ // Verify insertion was successful
177
+ if (!Array.isArray(inserted_tokens) || inserted_tokens.length === 0) {
178
+ const logger = create_app_logger();
179
+ const error_msg = `Failed to create ${token_type} token - no rows inserted`;
180
+ logger.error("token_service_insertion_failed", {
181
+ filename: "token_service.ts",
182
+ line_number: 0,
183
+ user_id,
184
+ token_type,
185
+ error: error_msg,
186
+ });
187
+ return {
188
+ success: false,
189
+ error: error_msg,
190
+ };
191
+ }
192
+
193
+ const logger = create_app_logger();
194
+ logger.info("token_service_token_created", {
195
+ filename: "token_service.ts",
196
+ line_number: 0,
197
+ user_id,
198
+ token_type,
199
+ });
200
+
201
+ // Log raw token and test URLs in debug mode (logger handles dev mode)
202
+ logger.debug("token_service_raw_token", {
203
+ filename: "token_service.ts",
204
+ line_number: 0,
205
+ user_id,
206
+ token_type,
207
+ raw_token,
208
+ test_url: token_type === "email_verification"
209
+ ? `/hazo_auth/verify_email?token=${raw_token}`
210
+ : token_type === "password_reset"
211
+ ? `/hazo_auth/reset_password?token=${raw_token}`
212
+ : undefined,
213
+ });
214
+
215
+ return {
216
+ success: true,
217
+ raw_token,
218
+ };
219
+ } catch (error) {
220
+ const logger = create_app_logger();
221
+ const error_message =
222
+ error instanceof Error ? error.message : "Unknown error";
223
+ const error_stack = error instanceof Error ? error.stack : undefined;
224
+
225
+ logger.error("token_service_create_token_error", {
226
+ filename: "token_service.ts",
227
+ line_number: 0,
228
+ user_id: params.user_id,
229
+ token_type: params.token_type,
230
+ error: error_message,
231
+ error_stack,
232
+ });
233
+
234
+ return {
235
+ success: false,
236
+ error: error_message,
237
+ };
238
+ }
239
+ }
240
+
@@ -0,0 +1,189 @@
1
+ // file_description: LRU cache implementation for hazo_get_user_profiles with TTL and size limits
2
+ // section: imports
3
+ import type { UserProfileInfo } from "./user_profiles_service";
4
+
5
+ // section: types
6
+
7
+ /**
8
+ * Cache entry structure for user profiles
9
+ */
10
+ type ProfileCacheEntry = {
11
+ profile: UserProfileInfo;
12
+ timestamp: number; // Unix timestamp in milliseconds
13
+ };
14
+
15
+ // section: cache_class
16
+
17
+ /**
18
+ * LRU cache implementation with TTL and size limits for user profiles
19
+ * Uses Map to maintain insertion order for LRU eviction
20
+ */
21
+ class UserProfilesCache {
22
+ private cache: Map<string, ProfileCacheEntry>;
23
+ private max_size: number;
24
+ private ttl_ms: number;
25
+
26
+ constructor(max_size: number, ttl_minutes: number) {
27
+ this.cache = new Map();
28
+ this.max_size = max_size;
29
+ this.ttl_ms = ttl_minutes * 60 * 1000;
30
+ }
31
+
32
+ /**
33
+ * Gets a cached profile for a user
34
+ * Returns undefined if not found or expired
35
+ * @param user_id - User ID to look up
36
+ * @returns Profile or undefined
37
+ */
38
+ get(user_id: string): UserProfileInfo | undefined {
39
+ const entry = this.cache.get(user_id);
40
+
41
+ if (!entry) {
42
+ return undefined;
43
+ }
44
+
45
+ const now = Date.now();
46
+ const age = now - entry.timestamp;
47
+
48
+ // Check if entry is expired
49
+ if (age > this.ttl_ms) {
50
+ this.cache.delete(user_id);
51
+ return undefined;
52
+ }
53
+
54
+ // Move to end (most recently used)
55
+ this.cache.delete(user_id);
56
+ this.cache.set(user_id, entry);
57
+
58
+ return entry.profile;
59
+ }
60
+
61
+ /**
62
+ * Gets multiple profiles from cache
63
+ * Returns object with found profiles and missing IDs
64
+ * @param user_ids - Array of user IDs to look up
65
+ * @returns Object with cached profiles and IDs not in cache
66
+ */
67
+ get_many(user_ids: string[]): {
68
+ cached: UserProfileInfo[];
69
+ missing_ids: string[];
70
+ } {
71
+ const cached: UserProfileInfo[] = [];
72
+ const missing_ids: string[] = [];
73
+
74
+ for (const user_id of user_ids) {
75
+ const profile = this.get(user_id);
76
+ if (profile) {
77
+ cached.push(profile);
78
+ } else {
79
+ missing_ids.push(user_id);
80
+ }
81
+ }
82
+
83
+ return { cached, missing_ids };
84
+ }
85
+
86
+ /**
87
+ * Sets a cache entry for a user profile
88
+ * Evicts least recently used entries if cache is full
89
+ * @param user_id - User ID
90
+ * @param profile - User profile data
91
+ */
92
+ set(user_id: string, profile: UserProfileInfo): void {
93
+ // Evict LRU entries if cache is full
94
+ while (this.cache.size >= this.max_size) {
95
+ const first_key = this.cache.keys().next().value;
96
+ if (first_key) {
97
+ this.cache.delete(first_key);
98
+ } else {
99
+ break;
100
+ }
101
+ }
102
+
103
+ const entry: ProfileCacheEntry = {
104
+ profile,
105
+ timestamp: Date.now(),
106
+ };
107
+
108
+ this.cache.set(user_id, entry);
109
+ }
110
+
111
+ /**
112
+ * Sets multiple cache entries at once
113
+ * @param profiles - Array of user profiles to cache
114
+ */
115
+ set_many(profiles: UserProfileInfo[]): void {
116
+ for (const profile of profiles) {
117
+ this.set(profile.user_id, profile);
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Invalidates cache for a specific user
123
+ * @param user_id - User ID to invalidate
124
+ */
125
+ invalidate_user(user_id: string): void {
126
+ this.cache.delete(user_id);
127
+ }
128
+
129
+ /**
130
+ * Invalidates cache for multiple users
131
+ * @param user_ids - Array of user IDs to invalidate
132
+ */
133
+ invalidate_users(user_ids: string[]): void {
134
+ for (const user_id of user_ids) {
135
+ this.cache.delete(user_id);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Invalidates all cache entries
141
+ */
142
+ invalidate_all(): void {
143
+ this.cache.clear();
144
+ }
145
+
146
+ /**
147
+ * Gets cache statistics
148
+ * @returns Object with cache size and max size
149
+ */
150
+ get_stats(): {
151
+ size: number;
152
+ max_size: number;
153
+ ttl_minutes: number;
154
+ } {
155
+ return {
156
+ size: this.cache.size,
157
+ max_size: this.max_size,
158
+ ttl_minutes: this.ttl_ms / 60000,
159
+ };
160
+ }
161
+ }
162
+
163
+ // section: singleton
164
+ // Global cache instance (initialized with defaults, will be configured on first use)
165
+ let user_profiles_cache_instance: UserProfilesCache | null = null;
166
+
167
+ /**
168
+ * Gets or creates the global user profiles cache instance
169
+ * @param max_size - Maximum cache size (default: 5000)
170
+ * @param ttl_minutes - TTL in minutes (default: 5)
171
+ * @returns User profiles cache instance
172
+ */
173
+ export function get_user_profiles_cache(
174
+ max_size: number = 5000,
175
+ ttl_minutes: number = 5,
176
+ ): UserProfilesCache {
177
+ if (!user_profiles_cache_instance) {
178
+ user_profiles_cache_instance = new UserProfilesCache(max_size, ttl_minutes);
179
+ }
180
+ return user_profiles_cache_instance;
181
+ }
182
+
183
+ /**
184
+ * Resets the global cache instance (useful for testing)
185
+ */
186
+ export function reset_user_profiles_cache(): void {
187
+ user_profiles_cache_instance = null;
188
+ }
189
+