whitebox-geolocation-maxmind
v0.1.0
Published
MaxMind GeoLite2/GeoIP2 provider for whitebox-pro-server-plugin-geolocation — local .mmdb file, in-process lookup, no per-request network call.
Downloads
131
Readme
whitebox-geolocation-maxmind
MaxMind provider for
whitebox-pro-server-plugin-geolocation— a local.mmdbfile, in-process lookup, no per-request network call.
Install
npm i whitebox-geolocation-maxmind @maxmind/geoip2-nodeDownload a GeoLite2-City.mmdb file from your MaxMind account (free, requires an account + license key — no attribution required). MaxMind ships GeoLite2 updates regularly; your deploy process is responsible for keeping the file current.
Keeping the database current
This package deliberately does not implement authenticated downloads,
checksums, or retry/backoff — MaxMind already ships and maintains
geoipupdate (their official CLI) for
exactly that job. Run it as a cron job or a sidecar container pointed at the same
dbPath, on whatever schedule matches how often you need fresh data (daily is
typical; GeoLite2 itself ships at most weekly).
That leaves one job for THIS provider: noticing that geoipupdate replaced the
file on disk and picking up the new data without a process restart. Pass watch
to turn that on:
import { geolocation } from 'whitebox-pro-server-plugin-geolocation'
import { maxmind } from 'whitebox-geolocation-maxmind'
geolocation({
provider: maxmind({
dbPath: process.env.GEOIP_DB_PATH,
watch: true, // poll the file's mtime every 5 min (default); or pass a number of ms for a custom interval
}),
})The poll only ever swaps in a new reader once it has opened successfully — a
transient failure (the file mid-swap, a corrupt download, a momentary stat error)
leaves the currently-serving reader untouched and just tries again on the next
tick. watch is opt-in; omit it and the file is read once at first lookup and
never re-checked, same as before.
What it returns
provider.lookup(ip)
// → { country: 'BG', region: 'Sofia-Capital', city: 'Sofia', lat: 42.6977, lon: 23.3219 }
// → null — no data for this IP (private/reserved/unroutable ranges included)Field mapping, straight off the GeoLite2 City response:
| contract field | source |
|---|---|
| country | response.country.iso_code |
| region | response.subdivisions[0].names.en (the first/most specific subdivision; null if the country has none) |
| city | response.city.names.en |
| lat/lon | response.location.latitude/.longitude |
Design notes
- Lazy, cached reader. The
.mmdbfile is opened on the firstlookup()call, not at construction — the plugin factory itself is synchronous, but opening the file is async. Every call after the first awaits the same already-open reader; the file is never re-read unlesswatchis on. - Not-found is
null, everything else propagates. AAddressNotFoundError(no entry for that IP — includes all private/loopback/reserved ranges) maps tonull, matching the provider contract. Any other error (a corrupt or missing.mmdbfile) is re-thrown —server-plugin-geolocationcatches and logs it there, so one bad lookup never breaks session resolution, but a systemic problem (wrongdbPath, corrupt download) is still visible in the logs rather than silently swallowed here. watchnever tears down a working reader on a hunch. Each poll tick builds the new reader fully before touching any shared state, and only swaps it in if that succeeds — a failed reload (mid-swap file, transient stat error) is silently ignored, leaving the last-known-good reader serving lookups until a later tick succeeds. The poll timer is.unref()'d, so it can never be the reason a process won't exit; callprovider.stopWatching()to clear it explicitly (mainly useful for tests or managing the provider's lifecycle outside the plugin).
