whitebox-pro-geolocation-maxmind
v0.2.0
Published
MaxMind GeoLite2/GeoIP2 provider for whitebox-pro-server-plugin-geolocation — local .mmdb file, in-process lookup, no per-request network call.
Readme
whitebox-pro-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-pro-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-pro-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.
Reporting the database's own health
A stale .mmdb degrades silently — it keeps answering every lookup with a
plausible-looking city, just from last year's IP allocations, and never raises an
error. So this provider exposes what it knows about the file it is serving from:
provider.health()
// → { provider: 'maxmind', dbPath: '/db/GeoLite2-City.mmdb',
// loaded: true, loadedAt: 1769000000000, mtimeMs: 1768900000000, watching: true }server-plugin-geolocation reads this in its status() and turns mtimeMs into
a database-age metric plus a stale flag for the monitoring board. It is
synchronous and does no I/O — it reports state already tracked, so a monitoring
call can't block on the same wedged disk it's asking about. mtimeMs is the mtime
of the file actually loaded, not whatever is on disk right now: with watch
on the poll keeps those the same, and with watch off the loaded file's mtime is
still the honest answer to "how old is the data we're serving".
health() is not part of the provider contract (that is name + lookup) — the
plugin calls it only if present, so a provider that can't describe its data
source simply contributes lookup counts.
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).
