@nauth-toolkit/core 0.1.0 → 0.1.5

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 (184) hide show
  1. package/LICENSE +90 -0
  2. package/README.md +9 -0
  3. package/package.json +8 -3
  4. package/jest.config.js +0 -15
  5. package/jest.setup.ts +0 -6
  6. package/src/adapters/database-columns.ts +0 -165
  7. package/src/adapters/express.adapter.ts +0 -385
  8. package/src/adapters/fastify.adapter.ts +0 -416
  9. package/src/adapters/index.ts +0 -16
  10. package/src/adapters/storage.factory.ts +0 -143
  11. package/src/bootstrap.ts +0 -374
  12. package/src/dto/auth-challenge.dto.ts +0 -231
  13. package/src/dto/auth-response.dto.ts +0 -253
  14. package/src/dto/challenge-response.dto.ts +0 -234
  15. package/src/dto/change-password-request.dto.ts +0 -50
  16. package/src/dto/change-password-response.dto.ts +0 -29
  17. package/src/dto/change-password.dto.ts +0 -57
  18. package/src/dto/error-response.dto.ts +0 -136
  19. package/src/dto/get-available-methods.dto.ts +0 -55
  20. package/src/dto/get-challenge-data-response.dto.ts +0 -28
  21. package/src/dto/get-challenge-data.dto.ts +0 -69
  22. package/src/dto/get-client-info.dto.ts +0 -104
  23. package/src/dto/get-device-token-response.dto.ts +0 -25
  24. package/src/dto/get-events-by-type.dto.ts +0 -76
  25. package/src/dto/get-ip-address-response.dto.ts +0 -24
  26. package/src/dto/get-mfa-status.dto.ts +0 -94
  27. package/src/dto/get-risk-assessment-history.dto.ts +0 -39
  28. package/src/dto/get-session-id-response.dto.ts +0 -25
  29. package/src/dto/get-setup-data-response.dto.ts +0 -31
  30. package/src/dto/get-setup-data.dto.ts +0 -75
  31. package/src/dto/get-suspicious-activity.dto.ts +0 -42
  32. package/src/dto/get-user-agent-response.dto.ts +0 -23
  33. package/src/dto/get-user-auth-history.dto.ts +0 -95
  34. package/src/dto/get-user-by-email.dto.ts +0 -61
  35. package/src/dto/get-user-by-id.dto.ts +0 -46
  36. package/src/dto/get-user-devices.dto.ts +0 -53
  37. package/src/dto/get-user-response.dto.ts +0 -17
  38. package/src/dto/has-provider.dto.ts +0 -56
  39. package/src/dto/index.ts +0 -57
  40. package/src/dto/is-trusted-device-response.dto.ts +0 -34
  41. package/src/dto/list-providers-response.dto.ts +0 -23
  42. package/src/dto/login.dto.ts +0 -95
  43. package/src/dto/logout-all-response.dto.ts +0 -24
  44. package/src/dto/logout-all.dto.ts +0 -65
  45. package/src/dto/logout-response.dto.ts +0 -25
  46. package/src/dto/logout.dto.ts +0 -64
  47. package/src/dto/refresh-token.dto.ts +0 -36
  48. package/src/dto/remove-devices.dto.ts +0 -85
  49. package/src/dto/resend-code-response.dto.ts +0 -32
  50. package/src/dto/resend-code.dto.ts +0 -51
  51. package/src/dto/reset-password.dto.ts +0 -115
  52. package/src/dto/respond-challenge.dto.ts +0 -272
  53. package/src/dto/set-mfa-exemption.dto.ts +0 -112
  54. package/src/dto/set-must-change-password-response.dto.ts +0 -27
  55. package/src/dto/set-must-change-password.dto.ts +0 -46
  56. package/src/dto/set-preferred-method.dto.ts +0 -80
  57. package/src/dto/setup-mfa.dto.ts +0 -98
  58. package/src/dto/signup.dto.ts +0 -174
  59. package/src/dto/social-auth.dto.ts +0 -422
  60. package/src/dto/trust-device-response.dto.ts +0 -30
  61. package/src/dto/trust-device.dto.ts +0 -9
  62. package/src/dto/update-user-attributes-request.dto.ts +0 -51
  63. package/src/dto/user-response.dto.ts +0 -138
  64. package/src/dto/user-update.dto.ts +0 -222
  65. package/src/dto/verify-email.dto.ts +0 -313
  66. package/src/dto/verify-mfa-code.dto.ts +0 -103
  67. package/src/dto/verify-phone-by-sub.dto.ts +0 -78
  68. package/src/dto/verify-phone.dto.ts +0 -245
  69. package/src/entities/auth-audit.entity.ts +0 -232
  70. package/src/entities/challenge-session.entity.ts +0 -116
  71. package/src/entities/index.ts +0 -29
  72. package/src/entities/login-attempt.entity.ts +0 -64
  73. package/src/entities/mfa-device.entity.ts +0 -151
  74. package/src/entities/rate-limit.entity.ts +0 -44
  75. package/src/entities/session.entity.ts +0 -180
  76. package/src/entities/social-account.entity.ts +0 -96
  77. package/src/entities/storage-lock.entity.ts +0 -39
  78. package/src/entities/trusted-device.entity.ts +0 -112
  79. package/src/entities/user.entity.ts +0 -243
  80. package/src/entities/verification-token.entity.ts +0 -141
  81. package/src/enums/auth-audit-event-type.enum.ts +0 -360
  82. package/src/enums/error-codes.enum.ts +0 -420
  83. package/src/enums/mfa-method.enum.ts +0 -97
  84. package/src/enums/risk-factor.enum.ts +0 -111
  85. package/src/exceptions/nauth.exception.ts +0 -231
  86. package/src/handlers/auth.handler.ts +0 -260
  87. package/src/handlers/client-info.handler.ts +0 -101
  88. package/src/handlers/csrf.handler.ts +0 -156
  89. package/src/handlers/token-delivery.handler.ts +0 -118
  90. package/src/index.ts +0 -118
  91. package/src/interfaces/client-info.interface.ts +0 -85
  92. package/src/interfaces/config.interface.ts +0 -2135
  93. package/src/interfaces/entities.interface.ts +0 -226
  94. package/src/interfaces/index.ts +0 -15
  95. package/src/interfaces/logger.interface.ts +0 -283
  96. package/src/interfaces/mfa-provider.interface.ts +0 -154
  97. package/src/interfaces/oauth.interface.ts +0 -148
  98. package/src/interfaces/provider.interface.ts +0 -47
  99. package/src/interfaces/social-auth-provider.interface.ts +0 -131
  100. package/src/interfaces/storage-adapter.interface.ts +0 -82
  101. package/src/interfaces/template.interface.ts +0 -510
  102. package/src/interfaces/token-verifier.interface.ts +0 -110
  103. package/src/internal.ts +0 -178
  104. package/src/platform/interfaces.ts +0 -299
  105. package/src/schemas/auth-config.schema.ts +0 -646
  106. package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
  107. package/src/services/adaptive-mfa-decision.service.ts +0 -457
  108. package/src/services/auth-audit.service.spec.ts +0 -675
  109. package/src/services/auth-audit.service.ts +0 -558
  110. package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
  111. package/src/services/auth-challenge-helper.service.ts +0 -825
  112. package/src/services/auth-flow-context-builder.service.ts +0 -520
  113. package/src/services/auth-flow-rules.ts +0 -202
  114. package/src/services/auth-flow-state-definitions.ts +0 -190
  115. package/src/services/auth-flow-state-machine.service.ts +0 -207
  116. package/src/services/auth-flow-state-machine.types.ts +0 -316
  117. package/src/services/auth.service.spec.ts +0 -4195
  118. package/src/services/auth.service.ts +0 -3727
  119. package/src/services/challenge.service.spec.ts +0 -1363
  120. package/src/services/challenge.service.ts +0 -696
  121. package/src/services/client-info.service.spec.ts +0 -572
  122. package/src/services/client-info.service.ts +0 -374
  123. package/src/services/csrf.service.ts +0 -54
  124. package/src/services/email-verification.service.spec.ts +0 -1229
  125. package/src/services/email-verification.service.ts +0 -578
  126. package/src/services/geo-location.service.spec.ts +0 -603
  127. package/src/services/geo-location.service.ts +0 -599
  128. package/src/services/index.ts +0 -13
  129. package/src/services/jwt.service.spec.ts +0 -882
  130. package/src/services/jwt.service.ts +0 -621
  131. package/src/services/mfa-base.service.spec.ts +0 -246
  132. package/src/services/mfa-base.service.ts +0 -611
  133. package/src/services/mfa.service.spec.ts +0 -693
  134. package/src/services/mfa.service.ts +0 -960
  135. package/src/services/password.service.spec.ts +0 -166
  136. package/src/services/password.service.ts +0 -309
  137. package/src/services/phone-verification.service.spec.ts +0 -1120
  138. package/src/services/phone-verification.service.ts +0 -751
  139. package/src/services/risk-detection.service.spec.ts +0 -1292
  140. package/src/services/risk-detection.service.ts +0 -1012
  141. package/src/services/risk-scoring.service.spec.ts +0 -204
  142. package/src/services/risk-scoring.service.ts +0 -131
  143. package/src/services/session.service.spec.ts +0 -1293
  144. package/src/services/session.service.ts +0 -803
  145. package/src/services/social-account.service.spec.ts +0 -725
  146. package/src/services/social-auth-base.service.spec.ts +0 -418
  147. package/src/services/social-auth-base.service.ts +0 -581
  148. package/src/services/social-auth.service.spec.ts +0 -238
  149. package/src/services/social-auth.service.ts +0 -436
  150. package/src/services/social-provider-registry.service.spec.ts +0 -238
  151. package/src/services/social-provider-registry.service.ts +0 -122
  152. package/src/services/trusted-device.service.spec.ts +0 -505
  153. package/src/services/trusted-device.service.ts +0 -339
  154. package/src/storage/account-lockout-storage.service.spec.ts +0 -310
  155. package/src/storage/account-lockout-storage.service.ts +0 -89
  156. package/src/storage/index.ts +0 -3
  157. package/src/storage/memory-storage.adapter.ts +0 -443
  158. package/src/storage/rate-limit-storage.service.spec.ts +0 -247
  159. package/src/storage/rate-limit-storage.service.ts +0 -38
  160. package/src/templates/html-template.engine.spec.ts +0 -161
  161. package/src/templates/html-template.engine.ts +0 -688
  162. package/src/templates/index.ts +0 -7
  163. package/src/utils/common-passwords.spec.ts +0 -230
  164. package/src/utils/common-passwords.ts +0 -170
  165. package/src/utils/context-storage.ts +0 -188
  166. package/src/utils/cookie-names.util.ts +0 -67
  167. package/src/utils/cookies.util.ts +0 -94
  168. package/src/utils/index.ts +0 -12
  169. package/src/utils/ip-extractor.spec.ts +0 -330
  170. package/src/utils/ip-extractor.ts +0 -220
  171. package/src/utils/nauth-logger.spec.ts +0 -388
  172. package/src/utils/nauth-logger.ts +0 -215
  173. package/src/utils/pii-redactor.spec.ts +0 -130
  174. package/src/utils/pii-redactor.ts +0 -288
  175. package/src/utils/setup/get-repositories.ts +0 -140
  176. package/src/utils/setup/init-services.ts +0 -422
  177. package/src/utils/setup/init-social.ts +0 -189
  178. package/src/utils/setup/init-storage.ts +0 -94
  179. package/src/utils/setup/register-mfa.ts +0 -165
  180. package/src/utils/setup/run-nauth-migrations.ts +0 -61
  181. package/src/utils/token-delivery-policy.ts +0 -38
  182. package/src/validators/template.validator.ts +0 -219
  183. package/tsconfig.json +0 -37
  184. package/tsconfig.lint.json +0 -6
@@ -1,599 +0,0 @@
1
- import * as fs from 'fs/promises';
2
- import * as path from 'path';
3
- import * as os from 'os';
4
- import { GeoLocationConfig, NAuthConfig } from '../interfaces/config.interface';
5
- import { StorageAdapter } from '../interfaces/storage-adapter.interface';
6
- import { NAuthLogger } from '../utils/nauth-logger';
7
- import { NAuthException } from '../exceptions/nauth.exception';
8
- import { AuthErrorCode } from '../enums/error-codes.enum';
9
- import { isPrivateIp } from '../utils/ip-extractor';
10
-
11
- // ============================================================================
12
- // Optional MaxMind Types
13
- // ============================================================================
14
-
15
- /**
16
- * MaxMind GeoIP2 Reader type (optional dependency)
17
- * Only available if @maxmind/geoip2-node is installed
18
- *
19
- * The Reader class has city() and country() methods that return response objects
20
- */
21
- type MaxMindReader = {
22
- city: (ip: string) => {
23
- country?: { isoCode?: string; names?: { en?: string }; isInEuropeanUnion?: boolean };
24
- city?: { names?: { en?: string } };
25
- subdivisions?: Array<{ names?: { en?: string } }>;
26
- postal?: { code?: string };
27
- location?: { latitude?: number; longitude?: number; timeZone?: string };
28
- continent?: { code?: string; names?: { en?: string } };
29
- };
30
- country: (ip: string) => {
31
- country?: { isoCode?: string; names?: { en?: string }; isInEuropeanUnion?: boolean };
32
- continent?: { code?: string; names?: { en?: string } };
33
- };
34
- };
35
-
36
- /**
37
- * MaxMind library module type (optional peer dependency)
38
- * Injected via dependency injection if package is installed
39
- */
40
- type MaxMindModule = {
41
- Reader: {
42
- open: (dbPath: string) => Promise<MaxMindReader>;
43
- };
44
- };
45
-
46
- /**
47
- * GeoLocation Service
48
- *
49
- * Provides IP geolocation using MaxMind GeoIP2 database files.
50
- * Platform-agnostic - works on all platforms where Node.js runs.
51
- *
52
- * Features:
53
- * - IP to country/city lookup from MaxMind .mmdb files
54
- * - Distributed locking for database updates (multi-server safe)
55
- * - Configurable database path (defaults to system temp directory)
56
- * - Graceful degradation if MaxMind not installed
57
- *
58
- * Requirements:
59
- * - @maxmind/geoip2-node peer dependency must be installed
60
- * - MaxMind license key and account ID for database downloads
61
- * - Storage adapter (for distributed locking)
62
- *
63
- * @example
64
- * ```typescript
65
- * // Get geolocation for an IP
66
- * const geo = await geoLocationService.getIpGeolocation('8.8.8.8');
67
- * console.log(geo.country); // 'US'
68
- * console.log(geo.city); // 'Mountain View'
69
- * ```
70
- */
71
- export class GeoLocationService {
72
- private readonly config: GeoLocationConfig['maxMind'];
73
- private readonly dbPath: string;
74
- private readonly maxMindLib: MaxMindModule | null;
75
- private cityReader: MaxMindReader | null = null;
76
- private countryReader: MaxMindReader | null = null;
77
- private readonly defaultEditions = ['GeoLite2-City', 'GeoLite2-Country'];
78
- private readonly lockKey = 'maxmind-db-update-lock';
79
- private readonly lockTtlSeconds = 300; // 5 minutes
80
-
81
- constructor(
82
- nauthConfig: NAuthConfig,
83
- private readonly storageAdapter: StorageAdapter,
84
- maxMindLib?: MaxMindModule | null,
85
- private readonly logger?: NAuthLogger,
86
- ) {
87
- // ============================================================================
88
- // Extract Configuration
89
- // ============================================================================
90
- this.config = nauthConfig.geoLocation?.maxMind;
91
-
92
- // ============================================================================
93
- // Initialize MaxMind Library (Optional)
94
- // ============================================================================
95
- this.maxMindLib = maxMindLib ?? null;
96
-
97
- if (!this.maxMindLib && this.config) {
98
- this.logger?.warn?.(
99
- 'MaxMind GeoIP2 is configured but @maxmind/geoip2-node package is not installed. ' +
100
- 'Install it with: yarn add @maxmind/geoip2-node\n' +
101
- 'Or remove geoLocation.maxMind from your configuration to disable geolocation.',
102
- );
103
- }
104
-
105
- // ============================================================================
106
- // Resolve Database Path
107
- // ============================================================================
108
- if (this.config?.dbPath) {
109
- // Use configured path (absolute or relative to cwd)
110
- this.dbPath = path.isAbsolute(this.config.dbPath)
111
- ? this.config.dbPath
112
- : path.resolve(process.cwd(), this.config.dbPath);
113
- } else {
114
- // Default to system temp directory
115
- const systemTemp = os.tmpdir();
116
- this.dbPath = path.join(systemTemp, 'nauth_maxmind');
117
- }
118
- }
119
-
120
- /**
121
- * Initialize service on module startup
122
- *
123
- * - Loads database files if they exist
124
- * - Optionally downloads databases if autoDownloadOnStartup is enabled
125
- */
126
- async onModuleInit(): Promise<void> {
127
- if (!this.config) {
128
- // No config provided - service disabled
129
- return;
130
- }
131
-
132
- if (!this.maxMindLib) {
133
- // MaxMind not installed - service disabled
134
- this.logger?.warn?.('MaxMind GeoIP2 library not available. Install @maxmind/geoip2-node to enable geolocation.');
135
- return;
136
- }
137
-
138
- // Ensure database directory exists
139
- await this.ensureDbDirectoryExists();
140
-
141
- // Load existing database files
142
- await this.loadDatabaseFiles();
143
-
144
- // Auto-download if enabled and files don't exist (only if downloads not skipped)
145
- if (!this.config.skipDownloads && this.config.autoDownloadOnStartup && (!this.cityReader || !this.countryReader)) {
146
- try {
147
- await this.updateGeoLocationDatabase();
148
- } catch (error) {
149
- this.logger?.warn?.(
150
- `Failed to auto-download MaxMind databases on startup: ${error instanceof Error ? error.message : 'Unknown error'}`,
151
- );
152
- }
153
- }
154
- }
155
-
156
- /**
157
- * Get geolocation information for an IP address
158
- *
159
- * @param ip - IP address to lookup
160
- * @returns Geolocation info with country, city, and coordinates (if available)
161
- *
162
- * @example
163
- * ```typescript
164
- * const geo = await geoLocationService.getIpGeolocation('8.8.8.8');
165
- * // { country: 'US', city: 'Mountain View', latitude: 37.386, longitude: -122.0838 }
166
- * ```
167
- */
168
- async getIpGeolocation(ip: string): Promise<{
169
- country?: string;
170
- city?: string;
171
- latitude?: number;
172
- longitude?: number
173
- }> {
174
- // ============================================================================
175
- // Check if Service is Available
176
- // ============================================================================
177
- if (!this.config || !this.maxMindLib) {
178
- // Service not configured or MaxMind not installed
179
- return {};
180
- }
181
-
182
- // ============================================================================
183
- // Skip Private IP Addresses
184
- // ============================================================================
185
- // MaxMind databases only contain public IP addresses. Private IPs (localhost,
186
- // 192.168.x.x, 10.x.x.x, etc.) will always fail lookup and generate unnecessary
187
- // error logs. Skip the lookup entirely for private IPs.
188
- if (isPrivateIp(ip)) {
189
- // Silently return empty result for private IPs (no lookup attempted)
190
- this.logger?.debug?.(`Skipping private IP ${ip} for geolocation lookup`);
191
- return {};
192
- }
193
-
194
- // ============================================================================
195
- // Try City Database First (More Detailed)
196
- // ============================================================================
197
- if (this.cityReader) {
198
- try {
199
- const result = this.cityReader.city(ip);
200
- const geoData = {
201
- country: result.country?.isoCode,
202
- city: result.city?.names?.en,
203
- latitude: result.location?.latitude,
204
- longitude: result.location?.longitude,
205
- };
206
-
207
- // Warn if coordinates are missing (useful for production debugging)
208
- if (!geoData.latitude || !geoData.longitude) {
209
- this.logger?.warn?.(
210
- `MaxMind city lookup for IP ${ip} returned city/country but NO coordinates: ` +
211
- `city=${geoData.city}, country=${geoData.country}`,
212
- );
213
- }
214
-
215
- return geoData;
216
- } catch (error) {
217
- // Non-fatal: Try country database (error logged at warn level if needed)
218
- }
219
- } else {
220
- this.logger?.warn?.(
221
- `MaxMind cityReader is not initialized for IP ${ip}. ` +
222
- `Only country database available (no coordinates).`,
223
- );
224
- }
225
-
226
- // ============================================================================
227
- // Fallback to Country Database
228
- // ============================================================================
229
- if (this.countryReader) {
230
- try {
231
- const result = this.countryReader.country(ip);
232
- return {
233
- country: result.country?.isoCode,
234
- };
235
- } catch (error) {
236
- // Non-fatal: Return empty result (error handled gracefully)
237
- }
238
- }
239
-
240
- // No databases loaded or lookup failed
241
- return {};
242
- }
243
-
244
- /**
245
- * Update MaxMind GeoIP2 database files
246
- *
247
- * Downloads the latest database files from MaxMind using distributed locking
248
- * to prevent concurrent downloads in multi-server deployments.
249
- *
250
- * Uses storage adapter for distributed locking:
251
- * - Lock key: 'maxmind-db-update-lock'
252
- * - Lock TTL: 5 minutes (300 seconds)
253
- * - Only one server/process can download at a time
254
- *
255
- * @throws {NAuthException} If MaxMind credentials are missing or download fails
256
- *
257
- * @example
258
- * ```typescript
259
- * // Call this method via cron job for periodic updates
260
- * await geoLocationService.updateGeoLocationDatabase();
261
- * ```
262
- */
263
- async updateGeoLocationDatabase(): Promise<void> {
264
- // ============================================================================
265
- // Validate Configuration
266
- // ============================================================================
267
- if (!this.config) {
268
- throw new NAuthException(AuthErrorCode.VALIDATION_FAILED, 'MaxMind configuration not provided');
269
- }
270
-
271
- if (!this.maxMindLib) {
272
- throw new NAuthException(
273
- AuthErrorCode.VALIDATION_FAILED,
274
- 'MaxMind library not available. Install @maxmind/geoip2-node peer dependency.',
275
- );
276
- }
277
-
278
- if (this.config.skipDownloads) {
279
- throw new NAuthException(
280
- AuthErrorCode.VALIDATION_FAILED,
281
- 'Database downloads are disabled (skipDownloads: true). Use existing files or disable skipDownloads to enable downloads.',
282
- );
283
- }
284
-
285
- if (!this.config.licenseKey || !this.config.accountId) {
286
- throw new NAuthException(
287
- AuthErrorCode.VALIDATION_FAILED,
288
- 'MaxMind licenseKey and accountId are required for database downloads',
289
- );
290
- }
291
-
292
- // ============================================================================
293
- // Acquire Distributed Lock (if using distributed storage)
294
- // ============================================================================
295
- // Memory storage: Lock works within single process (fine for single-server deployments)
296
- // Redis/Database storage: Lock works across multiple servers (prevents concurrent downloads)
297
- const lockAcquired = await this.storageAdapter.set(this.lockKey, `lock-${Date.now()}`, this.lockTtlSeconds, {
298
- nx: true,
299
- });
300
-
301
- if (!lockAcquired) {
302
- // Another process/server is already updating
303
- this.logger?.warn?.('MaxMind database update already in progress, skipping...');
304
- return;
305
- }
306
-
307
- try {
308
- // ============================================================================
309
- // Download and Update Databases
310
- // ============================================================================
311
- const editions = this.config.editions || this.defaultEditions;
312
- const accountId = this.config.accountId;
313
- const licenseKey = this.config.licenseKey;
314
-
315
- let successCount = 0;
316
- let failureCount = 0;
317
-
318
- for (const edition of editions) {
319
- try {
320
- await this.downloadDatabase(edition, accountId, licenseKey);
321
- successCount++;
322
- } catch (error) {
323
- failureCount++;
324
- this.logger?.error?.(
325
- `Failed to download MaxMind database ${edition}: ${error instanceof Error ? error.message : 'Unknown error'}`,
326
- );
327
- // Continue with other editions even if one fails
328
- }
329
- }
330
-
331
- // ============================================================================
332
- // Reload Database Files
333
- // ============================================================================
334
- await this.loadDatabaseFiles();
335
-
336
- // Only log success if at least one database was downloaded
337
- if (successCount > 0) {
338
- this.logger?.log?.(`MaxMind database update completed: ${successCount} succeeded, ${failureCount} failed`);
339
- } else if (failureCount > 0) {
340
- this.logger?.warn?.(
341
- `MaxMind database update failed: All ${failureCount} database(s) failed to download. See error messages above.`,
342
- );
343
- }
344
- } finally {
345
- // ============================================================================
346
- // Release Lock
347
- // ============================================================================
348
- try {
349
- await this.storageAdapter.del(this.lockKey);
350
- } catch (error) {
351
- // Non-fatal: Lock will expire automatically after TTL
352
- this.logger?.warn?.(
353
- `Failed to release MaxMind update lock: ${error instanceof Error ? error.message : 'Unknown error'}`,
354
- );
355
- }
356
- }
357
- }
358
-
359
- // ============================================================================
360
- // Private Helper Methods
361
- // ============================================================================
362
-
363
- /**
364
- * Ensure database directory exists
365
- *
366
- * Creates the directory if it doesn't exist.
367
- */
368
- private async ensureDbDirectoryExists(): Promise<void> {
369
- try {
370
- await fs.mkdir(this.dbPath, { recursive: true });
371
- this.logger?.debug?.(`MaxMind database directory: ${this.dbPath}`);
372
- } catch (error) {
373
- throw new NAuthException(
374
- AuthErrorCode.INTERNAL_ERROR,
375
- `Failed to create MaxMind database directory at ${this.dbPath}: ${error instanceof Error ? error.message : 'Unknown error'}`,
376
- );
377
- }
378
- }
379
-
380
- /**
381
- * Load database files from disk
382
- *
383
- * Loads .mmdb files for City and Country databases if they exist.
384
- */
385
- private async loadDatabaseFiles(): Promise<void> {
386
- if (!this.maxMindLib) {
387
- return;
388
- }
389
-
390
- try {
391
- // Try to load City database
392
- const cityDbPath = path.join(this.dbPath, 'GeoLite2-City.mmdb');
393
- try {
394
- // Reader.open returns a Reader instance with city()/country() methods
395
- this.cityReader = await this.maxMindLib.Reader.open(cityDbPath);
396
- if (typeof this.cityReader.city !== 'function') {
397
- this.logger?.warn?.('MaxMind Reader instance does not have city() method');
398
- this.cityReader = null;
399
- } else {
400
- this.logger?.debug?.('Loaded GeoLite2-City database');
401
- }
402
- } catch (error) {
403
- // City database not found or failed to load
404
- this.logger?.debug?.(
405
- `Failed to load City database: ${error instanceof Error ? error.message : 'Unknown error'}`,
406
- );
407
- this.cityReader = null;
408
- }
409
-
410
- // Try to load Country database
411
- const countryDbPath = path.join(this.dbPath, 'GeoLite2-Country.mmdb');
412
- try {
413
- this.countryReader = await this.maxMindLib.Reader.open(countryDbPath);
414
- if (typeof this.countryReader.country !== 'function') {
415
- this.logger?.warn?.('MaxMind Reader instance does not have country() method');
416
- this.countryReader = null;
417
- } else {
418
- this.logger?.debug?.('Loaded GeoLite2-Country database');
419
- }
420
- } catch (error) {
421
- // Country database not found or failed to load
422
- this.logger?.debug?.(
423
- `Failed to load Country database: ${error instanceof Error ? error.message : 'Unknown error'}`,
424
- );
425
- this.countryReader = null;
426
- }
427
-
428
- if (!this.cityReader && !this.countryReader) {
429
- if (this.config?.skipDownloads) {
430
- this.logger?.warn?.(
431
- `No MaxMind database files found in ${this.dbPath}. ` +
432
- `Downloads are disabled (skipDownloads: true). ` +
433
- `Ensure database files are available in the configured path, or set skipDownloads: false to enable downloads.`,
434
- );
435
- } else {
436
- this.logger?.warn?.(
437
- `No MaxMind database files found in ${this.dbPath}. ` +
438
- `Files will be downloaded when updateGeoLocationDatabase() is called or autoDownloadOnStartup is enabled.`,
439
- );
440
- }
441
- } else {
442
- // Log which databases were loaded
443
- const loaded = [];
444
- if (this.cityReader) loaded.push('GeoLite2-City');
445
- if (this.countryReader) loaded.push('GeoLite2-Country');
446
- this.logger?.debug?.(`Loaded MaxMind databases: ${loaded.join(', ')}`);
447
- }
448
- } catch (error) {
449
- this.logger?.warn?.(
450
- `Failed to load MaxMind database files: ${error instanceof Error ? error.message : 'Unknown error'}`,
451
- );
452
- }
453
- }
454
-
455
- /**
456
- * Download a MaxMind database file
457
- *
458
- * Downloads the specified edition from MaxMind's download API,
459
- * extracts the .mmdb file from the tar.gz archive, and saves it.
460
- *
461
- * Uses Node.js built-in zlib for gzip decompression and implements
462
- * basic tar parsing to extract the .mmdb file.
463
- *
464
- * @param edition - Edition name (e.g., 'GeoLite2-City')
465
- * @param accountId - MaxMind account ID
466
- * @param licenseKey - MaxMind license key
467
- */
468
- private async downloadDatabase(edition: string, _accountId: number, licenseKey: string): Promise<void> {
469
- // MaxMind Download API URL
470
- const url = `https://download.maxmind.com/app/geoip_download?edition_id=${edition}&license_key=${licenseKey}&suffix=tar.gz`;
471
- const tempTarPath = path.join(this.dbPath, `${edition}.tar.gz`);
472
- const outputPath = path.join(this.dbPath, `${edition}.mmdb`);
473
-
474
- try {
475
- // ============================================================================
476
- // Download tar.gz file
477
- // ============================================================================
478
- this.logger?.debug?.(`Downloading MaxMind database ${edition}...`);
479
-
480
- // Wrap fetch in timeout and better error handling
481
- let response: Response;
482
- try {
483
- response = await fetch(url, {
484
- // Add timeout to prevent hanging requests
485
- signal: AbortSignal.timeout(30000), // 30 second timeout
486
- });
487
- } catch (fetchError: any) {
488
- // Handle network errors (DNS failures, connection errors, etc.)
489
- if (
490
- fetchError?.code === 'ENOTFOUND' ||
491
- fetchError?.code === 'ECONNREFUSED' ||
492
- fetchError?.name === 'AbortError'
493
- ) {
494
- throw new NAuthException(
495
- AuthErrorCode.INTERNAL_ERROR,
496
- `Network error while downloading MaxMind database ${edition}: ${fetchError.message || 'DNS lookup failed or connection refused'}. Check your network connection and proxy settings.`,
497
- );
498
- }
499
- throw fetchError;
500
- }
501
-
502
- if (!response.ok) {
503
- throw new Error(`MaxMind API returned ${response.status}: ${response.statusText}`);
504
- }
505
-
506
- const buffer = Buffer.from(await response.arrayBuffer());
507
- await fs.writeFile(tempTarPath, buffer);
508
-
509
- // ============================================================================
510
- // Extract .mmdb file from tar.gz
511
- // ============================================================================
512
- // MaxMind tar.gz contains: <edition>_<date>/<edition>.mmdb
513
- // We need to decompress gzip, then parse tar to find the .mmdb file
514
- await this.extractTarGz(tempTarPath, outputPath, edition);
515
-
516
- // Clean up temp tar.gz file
517
- await fs.unlink(tempTarPath).catch(() => {
518
- // Ignore cleanup errors
519
- });
520
-
521
- this.logger?.debug?.(`Successfully downloaded and extracted ${edition}`);
522
- } catch (error) {
523
- // Clean up temp file on error
524
- await fs.unlink(tempTarPath).catch(() => {
525
- // Ignore cleanup errors
526
- });
527
-
528
- if (error instanceof NAuthException) {
529
- throw error;
530
- }
531
-
532
- throw new NAuthException(
533
- AuthErrorCode.INTERNAL_ERROR,
534
- `Failed to download MaxMind database ${edition}: ${error instanceof Error ? error.message : 'Unknown error'}`,
535
- );
536
- }
537
- }
538
-
539
- /**
540
- * Extract .mmdb file from tar.gz archive
541
- *
542
- * Uses Node.js built-in zlib for gzip decompression and implements
543
- * basic tar parsing to find and extract the .mmdb file.
544
- *
545
- * @param tarGzPath - Path to the .tar.gz file
546
- * @param outputPath - Path where .mmdb file should be saved
547
- * @param edition - Edition name (to find correct file in archive)
548
- */
549
- private async extractTarGz(tarGzPath: string, outputPath: string, edition: string): Promise<void> {
550
- // Prefer OS tar (no extra deps) per project guidance
551
- const { exec } = await import('child_process');
552
- const { promisify } = await import('util');
553
- const execAsync = promisify(exec);
554
-
555
- // Use a dedicated extraction directory to avoid polluting dbPath
556
- const extractDir = path.join(this.dbPath, `extract_${edition}_${Date.now()}`);
557
- await fs.mkdir(extractDir, { recursive: true });
558
-
559
- try {
560
- // Extract archive
561
- await execAsync(`tar -xzf "${tarGzPath}" -C "${extractDir}"`);
562
-
563
- // Find the .mmdb file (usually in a subdirectory like GeoLite2-City_YYYYMMDD/GeoLite2-City.mmdb)
564
- const entries = await fs.readdir(extractDir);
565
- let mmdbPath: string | null = null;
566
- for (const entry of entries) {
567
- const entryPath = path.join(extractDir, entry);
568
- const stat = await fs.stat(entryPath);
569
- if (stat.isDirectory()) {
570
- const inner = await fs.readdir(entryPath);
571
- for (const f of inner) {
572
- if (f.endsWith('.mmdb') && f.includes(edition)) {
573
- mmdbPath = path.join(entryPath, f);
574
- break;
575
- }
576
- }
577
- } else if (entry.endsWith('.mmdb') && entry.includes(edition)) {
578
- mmdbPath = entryPath;
579
- }
580
- if (mmdbPath) break;
581
- }
582
-
583
- if (!mmdbPath) {
584
- throw new Error(`Could not find extracted .mmdb file for ${edition}`);
585
- }
586
-
587
- // Move to final location
588
- await fs.rename(mmdbPath, outputPath);
589
- } finally {
590
- // Cleanup extraction directory
591
- try {
592
- // Best-effort cleanup
593
- await execAsync(`rm -rf "${extractDir}"`).catch(() => undefined);
594
- } catch {
595
- // ignore
596
- }
597
- }
598
- }
599
- }
@@ -1,13 +0,0 @@
1
- export * from './password.service';
2
- export * from './jwt.service';
3
- export * from './session.service';
4
- export * from './auth.service';
5
- export * from './email-verification.service';
6
- export * from './client-info.service';
7
- export * from './challenge.service';
8
- export * from './auth-challenge-helper.service';
9
- export * from './mfa-base.service';
10
- export * from './mfa.service';
11
- export * from './risk-detection.service';
12
- export * from './risk-scoring.service';
13
- export * from './adaptive-mfa-decision.service';