@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.
- package/LICENSE +90 -0
- package/README.md +9 -0
- package/package.json +8 -3
- package/jest.config.js +0 -15
- package/jest.setup.ts +0 -6
- package/src/adapters/database-columns.ts +0 -165
- package/src/adapters/express.adapter.ts +0 -385
- package/src/adapters/fastify.adapter.ts +0 -416
- package/src/adapters/index.ts +0 -16
- package/src/adapters/storage.factory.ts +0 -143
- package/src/bootstrap.ts +0 -374
- package/src/dto/auth-challenge.dto.ts +0 -231
- package/src/dto/auth-response.dto.ts +0 -253
- package/src/dto/challenge-response.dto.ts +0 -234
- package/src/dto/change-password-request.dto.ts +0 -50
- package/src/dto/change-password-response.dto.ts +0 -29
- package/src/dto/change-password.dto.ts +0 -57
- package/src/dto/error-response.dto.ts +0 -136
- package/src/dto/get-available-methods.dto.ts +0 -55
- package/src/dto/get-challenge-data-response.dto.ts +0 -28
- package/src/dto/get-challenge-data.dto.ts +0 -69
- package/src/dto/get-client-info.dto.ts +0 -104
- package/src/dto/get-device-token-response.dto.ts +0 -25
- package/src/dto/get-events-by-type.dto.ts +0 -76
- package/src/dto/get-ip-address-response.dto.ts +0 -24
- package/src/dto/get-mfa-status.dto.ts +0 -94
- package/src/dto/get-risk-assessment-history.dto.ts +0 -39
- package/src/dto/get-session-id-response.dto.ts +0 -25
- package/src/dto/get-setup-data-response.dto.ts +0 -31
- package/src/dto/get-setup-data.dto.ts +0 -75
- package/src/dto/get-suspicious-activity.dto.ts +0 -42
- package/src/dto/get-user-agent-response.dto.ts +0 -23
- package/src/dto/get-user-auth-history.dto.ts +0 -95
- package/src/dto/get-user-by-email.dto.ts +0 -61
- package/src/dto/get-user-by-id.dto.ts +0 -46
- package/src/dto/get-user-devices.dto.ts +0 -53
- package/src/dto/get-user-response.dto.ts +0 -17
- package/src/dto/has-provider.dto.ts +0 -56
- package/src/dto/index.ts +0 -57
- package/src/dto/is-trusted-device-response.dto.ts +0 -34
- package/src/dto/list-providers-response.dto.ts +0 -23
- package/src/dto/login.dto.ts +0 -95
- package/src/dto/logout-all-response.dto.ts +0 -24
- package/src/dto/logout-all.dto.ts +0 -65
- package/src/dto/logout-response.dto.ts +0 -25
- package/src/dto/logout.dto.ts +0 -64
- package/src/dto/refresh-token.dto.ts +0 -36
- package/src/dto/remove-devices.dto.ts +0 -85
- package/src/dto/resend-code-response.dto.ts +0 -32
- package/src/dto/resend-code.dto.ts +0 -51
- package/src/dto/reset-password.dto.ts +0 -115
- package/src/dto/respond-challenge.dto.ts +0 -272
- package/src/dto/set-mfa-exemption.dto.ts +0 -112
- package/src/dto/set-must-change-password-response.dto.ts +0 -27
- package/src/dto/set-must-change-password.dto.ts +0 -46
- package/src/dto/set-preferred-method.dto.ts +0 -80
- package/src/dto/setup-mfa.dto.ts +0 -98
- package/src/dto/signup.dto.ts +0 -174
- package/src/dto/social-auth.dto.ts +0 -422
- package/src/dto/trust-device-response.dto.ts +0 -30
- package/src/dto/trust-device.dto.ts +0 -9
- package/src/dto/update-user-attributes-request.dto.ts +0 -51
- package/src/dto/user-response.dto.ts +0 -138
- package/src/dto/user-update.dto.ts +0 -222
- package/src/dto/verify-email.dto.ts +0 -313
- package/src/dto/verify-mfa-code.dto.ts +0 -103
- package/src/dto/verify-phone-by-sub.dto.ts +0 -78
- package/src/dto/verify-phone.dto.ts +0 -245
- package/src/entities/auth-audit.entity.ts +0 -232
- package/src/entities/challenge-session.entity.ts +0 -116
- package/src/entities/index.ts +0 -29
- package/src/entities/login-attempt.entity.ts +0 -64
- package/src/entities/mfa-device.entity.ts +0 -151
- package/src/entities/rate-limit.entity.ts +0 -44
- package/src/entities/session.entity.ts +0 -180
- package/src/entities/social-account.entity.ts +0 -96
- package/src/entities/storage-lock.entity.ts +0 -39
- package/src/entities/trusted-device.entity.ts +0 -112
- package/src/entities/user.entity.ts +0 -243
- package/src/entities/verification-token.entity.ts +0 -141
- package/src/enums/auth-audit-event-type.enum.ts +0 -360
- package/src/enums/error-codes.enum.ts +0 -420
- package/src/enums/mfa-method.enum.ts +0 -97
- package/src/enums/risk-factor.enum.ts +0 -111
- package/src/exceptions/nauth.exception.ts +0 -231
- package/src/handlers/auth.handler.ts +0 -260
- package/src/handlers/client-info.handler.ts +0 -101
- package/src/handlers/csrf.handler.ts +0 -156
- package/src/handlers/token-delivery.handler.ts +0 -118
- package/src/index.ts +0 -118
- package/src/interfaces/client-info.interface.ts +0 -85
- package/src/interfaces/config.interface.ts +0 -2135
- package/src/interfaces/entities.interface.ts +0 -226
- package/src/interfaces/index.ts +0 -15
- package/src/interfaces/logger.interface.ts +0 -283
- package/src/interfaces/mfa-provider.interface.ts +0 -154
- package/src/interfaces/oauth.interface.ts +0 -148
- package/src/interfaces/provider.interface.ts +0 -47
- package/src/interfaces/social-auth-provider.interface.ts +0 -131
- package/src/interfaces/storage-adapter.interface.ts +0 -82
- package/src/interfaces/template.interface.ts +0 -510
- package/src/interfaces/token-verifier.interface.ts +0 -110
- package/src/internal.ts +0 -178
- package/src/platform/interfaces.ts +0 -299
- package/src/schemas/auth-config.schema.ts +0 -646
- package/src/services/adaptive-mfa-decision.service.spec.ts +0 -1058
- package/src/services/adaptive-mfa-decision.service.ts +0 -457
- package/src/services/auth-audit.service.spec.ts +0 -675
- package/src/services/auth-audit.service.ts +0 -558
- package/src/services/auth-challenge-helper.service.spec.ts +0 -3227
- package/src/services/auth-challenge-helper.service.ts +0 -825
- package/src/services/auth-flow-context-builder.service.ts +0 -520
- package/src/services/auth-flow-rules.ts +0 -202
- package/src/services/auth-flow-state-definitions.ts +0 -190
- package/src/services/auth-flow-state-machine.service.ts +0 -207
- package/src/services/auth-flow-state-machine.types.ts +0 -316
- package/src/services/auth.service.spec.ts +0 -4195
- package/src/services/auth.service.ts +0 -3727
- package/src/services/challenge.service.spec.ts +0 -1363
- package/src/services/challenge.service.ts +0 -696
- package/src/services/client-info.service.spec.ts +0 -572
- package/src/services/client-info.service.ts +0 -374
- package/src/services/csrf.service.ts +0 -54
- package/src/services/email-verification.service.spec.ts +0 -1229
- package/src/services/email-verification.service.ts +0 -578
- package/src/services/geo-location.service.spec.ts +0 -603
- package/src/services/geo-location.service.ts +0 -599
- package/src/services/index.ts +0 -13
- package/src/services/jwt.service.spec.ts +0 -882
- package/src/services/jwt.service.ts +0 -621
- package/src/services/mfa-base.service.spec.ts +0 -246
- package/src/services/mfa-base.service.ts +0 -611
- package/src/services/mfa.service.spec.ts +0 -693
- package/src/services/mfa.service.ts +0 -960
- package/src/services/password.service.spec.ts +0 -166
- package/src/services/password.service.ts +0 -309
- package/src/services/phone-verification.service.spec.ts +0 -1120
- package/src/services/phone-verification.service.ts +0 -751
- package/src/services/risk-detection.service.spec.ts +0 -1292
- package/src/services/risk-detection.service.ts +0 -1012
- package/src/services/risk-scoring.service.spec.ts +0 -204
- package/src/services/risk-scoring.service.ts +0 -131
- package/src/services/session.service.spec.ts +0 -1293
- package/src/services/session.service.ts +0 -803
- package/src/services/social-account.service.spec.ts +0 -725
- package/src/services/social-auth-base.service.spec.ts +0 -418
- package/src/services/social-auth-base.service.ts +0 -581
- package/src/services/social-auth.service.spec.ts +0 -238
- package/src/services/social-auth.service.ts +0 -436
- package/src/services/social-provider-registry.service.spec.ts +0 -238
- package/src/services/social-provider-registry.service.ts +0 -122
- package/src/services/trusted-device.service.spec.ts +0 -505
- package/src/services/trusted-device.service.ts +0 -339
- package/src/storage/account-lockout-storage.service.spec.ts +0 -310
- package/src/storage/account-lockout-storage.service.ts +0 -89
- package/src/storage/index.ts +0 -3
- package/src/storage/memory-storage.adapter.ts +0 -443
- package/src/storage/rate-limit-storage.service.spec.ts +0 -247
- package/src/storage/rate-limit-storage.service.ts +0 -38
- package/src/templates/html-template.engine.spec.ts +0 -161
- package/src/templates/html-template.engine.ts +0 -688
- package/src/templates/index.ts +0 -7
- package/src/utils/common-passwords.spec.ts +0 -230
- package/src/utils/common-passwords.ts +0 -170
- package/src/utils/context-storage.ts +0 -188
- package/src/utils/cookie-names.util.ts +0 -67
- package/src/utils/cookies.util.ts +0 -94
- package/src/utils/index.ts +0 -12
- package/src/utils/ip-extractor.spec.ts +0 -330
- package/src/utils/ip-extractor.ts +0 -220
- package/src/utils/nauth-logger.spec.ts +0 -388
- package/src/utils/nauth-logger.ts +0 -215
- package/src/utils/pii-redactor.spec.ts +0 -130
- package/src/utils/pii-redactor.ts +0 -288
- package/src/utils/setup/get-repositories.ts +0 -140
- package/src/utils/setup/init-services.ts +0 -422
- package/src/utils/setup/init-social.ts +0 -189
- package/src/utils/setup/init-storage.ts +0 -94
- package/src/utils/setup/register-mfa.ts +0 -165
- package/src/utils/setup/run-nauth-migrations.ts +0 -61
- package/src/utils/token-delivery-policy.ts +0 -38
- package/src/validators/template.validator.ts +0 -219
- package/tsconfig.json +0 -37
- 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
|
-
}
|
package/src/services/index.ts
DELETED
|
@@ -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';
|