import { NAuthConfig } from '../interfaces/config.interface'; import { StorageAdapter } from '../interfaces/storage-adapter.interface'; import { NAuthLogger } from '../utils/nauth-logger'; /** * MaxMind GeoIP2 Reader type (optional dependency) * Only available if @maxmind/geoip2-node is installed * * The Reader class has city() and country() methods that return response objects */ type MaxMindReader = { city: (ip: string) => { country?: { isoCode?: string; names?: { en?: string; }; isInEuropeanUnion?: boolean; }; city?: { names?: { en?: string; }; }; subdivisions?: Array<{ names?: { en?: string; }; }>; postal?: { code?: string; }; location?: { latitude?: number; longitude?: number; timeZone?: string; }; continent?: { code?: string; names?: { en?: string; }; }; }; country: (ip: string) => { country?: { isoCode?: string; names?: { en?: string; }; isInEuropeanUnion?: boolean; }; continent?: { code?: string; names?: { en?: string; }; }; }; }; /** * MaxMind library module type (optional peer dependency) * Injected via dependency injection if package is installed */ type MaxMindModule = { Reader: { open: (dbPath: string) => Promise; }; }; /** * GeoLocation Service * * Provides IP geolocation using MaxMind GeoIP2 database files. * Platform-agnostic - works on all platforms where Node.js runs. * * Features: * - IP to country/city lookup from MaxMind .mmdb files * - Distributed locking for database updates (multi-server safe) * - Configurable database path (defaults to system temp directory) * - Graceful degradation if MaxMind not installed * * Requirements: * - @maxmind/geoip2-node peer dependency must be installed * - MaxMind license key and account ID for database downloads * - Storage adapter (for distributed locking) * * @example * ```typescript * // Get geolocation for an IP * const geo = await geoLocationService.getIpGeolocation('8.8.8.8'); * console.log(geo.country); // 'US' * console.log(geo.city); // 'Mountain View' * ``` */ export declare class GeoLocationService { private readonly storageAdapter; private readonly logger?; private readonly config; private readonly dbPath; private readonly maxMindLib; private cityReader; private countryReader; private readonly defaultEditions; /** Editions the service actually opens a reader for. */ private readonly loadableEditions; private readonly lockKey; private readonly lockTtlSeconds; /** How long to wait for another instance holding the download lock. */ private readonly lockWaitMs; /** Reuse existing .mmdb files younger than this; GeoLite2 publishes twice a week. */ private readonly minRefreshIntervalHours; private readonly lockRetryDelayMs; private updateInFlight; constructor(nauthConfig: NAuthConfig, storageAdapter: StorageAdapter, maxMindLib?: MaxMindModule | null, logger?: NAuthLogger | undefined); /** * Whether startup must fail when no database could be loaded. * * Defaults to true once a custom `downloadUrl` is configured: mirroring the database * yourself is a deliberate act, and booting without it silently answers every lookup * with `{}`. The MaxMind-API path keeps its historical warn-and-continue default so * existing deployments are unaffected. * * @returns True when a missing database should abort startup */ private get requireDatabaseOnStartup(); /** * Initialize service on module startup * * Awaited by `NAuth.create()` and by the NestJS lifecycle, so a download started here * completes before the instance serves traffic. Readers are replaced in place once the * download lands - lookups made after this resolves always see the loaded database. * * - Loads database files if they exist * - Downloads them when a `download` source is configured with `onStartup` (default true) * - Throws instead of warning when {@link requireDatabaseOnStartup} applies * * @throws {NAuthException} If no database could be loaded and startup requires one */ onModuleInit(): Promise; /** * Get geolocation information for an IP address * * @param ip - IP address to lookup * @returns Geolocation info with country, city, and coordinates (if available) * * @example * ```typescript * const geo = await geoLocationService.getIpGeolocation('8.8.8.8'); * // { country: 'US', city: 'Mountain View', latitude: 37.386, longitude: -122.0838 } * ``` */ getIpGeolocation(ip: string): Promise<{ country?: string; city?: string; latitude?: number; longitude?: number; }>; /** * Reload MaxMind database files from disk * * Reloads .mmdb files from the configured dbPath without downloading. * Useful when database files are managed externally (e.g., via geoipupdate, * cron jobs, or container volume updates). * * This method will: * - Attempt to load GeoLite2-City.mmdb * - Attempt to load GeoLite2-Country.mmdb * - Replace in-memory database readers with newly loaded ones * - Log warnings if no database files are found * * Safe to call repeatedly - if files haven't changed, it just reloads the same data. * * @example * ```typescript * // After external process updates database files * await geoLocationService.reloadGeoLocationDatabaseFromDisk(); * ``` * * @example * ```typescript * // In a NestJS scheduled job * @Cron('0 0 * * *') * async reloadGeoDb() { * await this.geoLocationService.reloadGeoLocationDatabaseFromDisk(); * } * ``` */ reloadGeoLocationDatabaseFromDisk(): Promise; /** * Update MaxMind GeoIP2 database files * * Downloads the latest database files from MaxMind, then reloads the in-memory * database readers. * * **Cluster behaviour (ECS tasks, Kubernetes pods, multiple servers):** * Downloads are *serialized*, not skipped. Instances take turns behind a distributed * lock held in the storage adapter (Redis or database), and each one re-checks * `dbPath` before downloading: * - **Shared volume** (EFS, NFS, mounted PVC): the first instance downloads, the rest * find fresh files and load them without touching MaxMind's API. * - **Container-local path** (the default, `os.tmpdir()`): each instance downloads its * own copy when its turn comes, so no instance is left without geolocation data. * * Concurrent calls within a single process share one run. * * Lock details: * - Lock key: 'maxmind-db-update-lock' * - Lock TTL: 5 minutes (300 seconds), so a crashed instance cannot wedge the cluster * - Waits up to 2 minutes for another instance, then downloads anyway rather than * starting without geolocation data * - Files younger than 24 hours are reused instead of re-downloaded * * After a successful download, the in-memory database readers are automatically * updated to use the new files. * * @throws {NAuthException} If MaxMind credentials are missing or download fails * * @example * ```typescript * // Call this method via cron job for periodic updates * await geoLocationService.updateGeoLocationDatabase(); * ``` */ updateGeoLocationDatabase(): Promise; /** * Perform one database update, serialized against other instances. * * @remarks * Assumes configuration has already been validated by * {@link GeoLocationService.updateGeoLocationDatabase}. */ private runDatabaseUpdate; /** * Release the distributed update lock, but only if this instance still owns it. * * @remarks * If the download outran the lock TTL another instance may already hold the lock; * deleting it unconditionally would let a third instance download concurrently. * The read-then-delete is not atomic, so this narrows the window rather than closing * it — the TTL remains the backstop. * * @param lockToken - Token written when the lock was acquired */ private releaseUpdateLock; /** * Load database files from disk if they are present and fresh enough to reuse. * * @remarks * Freshness is judged by file mtime. Every configured edition must be present and * fresh, otherwise a download is still needed. * * @returns True if fresh files were found and at least one reader was loaded */ private loadFreshDatabasesFromDisk; /** * Pause for the given number of milliseconds. * * @param ms - Delay in milliseconds */ private delay; /** * Ensure database directory exists * * Creates the directory if it doesn't exist. */ private ensureDbDirectoryExists; /** * Load database files from disk * * Loads .mmdb files for City and Country databases if they exist. */ private loadDatabaseFiles; /** * Resolve where a given edition should be downloaded from. * * Returns the configured mirror when `downloadUrl` is set, otherwise MaxMind's own * download API. * * @param edition - Edition name (e.g., 'GeoLite2-City') * @param licenseKey - MaxMind license key, used only for the MaxMind API URL * @returns The resolved URL, and whether it came from consumer configuration * @throws {NAuthException} If a configured URL is missing for the edition or uses an unsupported scheme */ private resolveDownloadUrl; /** * Reject download URLs the toolkit cannot fetch. * * Only HTTPS is accepted, plus plain HTTP to loopback for local development. `s3://` * is called out by name because it is the natural thing to reach for and would * otherwise fail with an opaque parse error. * * @param url - Configured URL * @param edition - Edition the URL belongs to, for the error message * @throws {NAuthException} If the scheme is unsupported or the URL is malformed */ private assertSupportedDownloadUrl; /** * Build request headers for a download from the configured mirror. * * @returns Headers including HTTP Basic credentials when configured */ private buildDownloadHeaders; /** * Download a MaxMind database file * * Downloads the specified edition from MaxMind's download API, * extracts the .mmdb file from the tar.gz archive, and saves it. * * Uses Node.js built-in zlib for gzip decompression and implements * basic tar parsing to extract the .mmdb file. * * @param edition - Edition name (e.g., 'GeoLite2-City') * @param licenseKey - MaxMind license key, unused when a mirror is configured */ private downloadDatabase; /** * Extract .mmdb file from tar.gz archive * * Uses Node.js built-in zlib for gzip decompression and implements * basic tar parsing to find and extract the .mmdb file. * * @param tarGzPath - Path to the .tar.gz file * @param outputPath - Path where .mmdb file should be saved * @param edition - Edition name (to find correct file in archive) */ private extractTarGz; } export {}; //# sourceMappingURL=geo-location.service.d.ts.map