@oschwald/maxminddb 0.1.0
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/CHANGELOG.md +64 -0
- package/LICENSE +15 -0
- package/README.md +187 -0
- package/RELEASE.md +93 -0
- package/index.d.ts +272 -0
- package/index.darwin-arm64.node +0 -0
- package/index.darwin-x64.node +0 -0
- package/index.js +547 -0
- package/index.linux-arm64-gnu.node +0 -0
- package/index.linux-x64-gnu.node +0 -0
- package/index.win32-arm64-msvc.node +0 -0
- package/index.win32-x64-msvc.node +0 -0
- package/package.json +61 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-06-14
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial Rust-backed Node.js module for MaxMind DB files, published as
|
|
13
|
+
`@oschwald/maxminddb`.
|
|
14
|
+
- Added a `node-maxmind`-compatible API with `open()`, `Reader`, `get()`,
|
|
15
|
+
`getWithPrefixLength()`, `load()`, `metadata`, `validate()`, and legacy
|
|
16
|
+
`init()`/`openSync()` error behavior.
|
|
17
|
+
- Added file-backed and buffer-backed readers with `MODE_AUTO`, `MODE_MMAP`,
|
|
18
|
+
`MODE_MEMORY`, and `MODE_BUFFER` open modes.
|
|
19
|
+
- Added optional watched reloads with serialized reload handling,
|
|
20
|
+
`lastReloadError`, and explicit watcher cleanup on `reader.close()`.
|
|
21
|
+
- Added native LRU caching of materialized records with `cacheStats()` and
|
|
22
|
+
`clearCache()`.
|
|
23
|
+
- Added path lookup extensions with `getPath()`, `getManyPath()`, and compiled
|
|
24
|
+
`reader.path()` lookups.
|
|
25
|
+
- Added batch lookup support with `getMany()`.
|
|
26
|
+
- Added lazy network iteration via native cursors, including `networks()`,
|
|
27
|
+
`within()`, `networkPages()`, `withinPages()`, and `NetworkIterator#nextPage()`.
|
|
28
|
+
- Added TypeScript declarations for the public API.
|
|
29
|
+
- Added benchmark tooling for comparing throughput against `node-maxmind`.
|
|
30
|
+
- Added npm trusted publishing with prebuilt native binaries for Linux x64 GNU,
|
|
31
|
+
Linux arm64 GNU, macOS x64, macOS arm64, Windows x64 MSVC, and Windows arm64
|
|
32
|
+
MSVC.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- The npm package is scoped as `@oschwald/maxminddb`; the Rust crate package is named
|
|
37
|
+
`maxminddb-node` to avoid colliding with the upstream Rust `maxminddb` crate.
|
|
38
|
+
- Package metadata includes repository, homepage, bugs, author, export map, and
|
|
39
|
+
ISC license metadata.
|
|
40
|
+
- Requires Node.js 20 or newer.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- Rejected gzip database inputs before opening.
|
|
45
|
+
- Hardened watched reloads so failed reloads keep the existing reader active.
|
|
46
|
+
- Hardened streamed large-file reads so truncated or growing files are rejected
|
|
47
|
+
instead of returning partially initialized buffers.
|
|
48
|
+
- Added decode corpus regressions for mixed MaxMind DB value types.
|
|
49
|
+
|
|
50
|
+
### Performance
|
|
51
|
+
|
|
52
|
+
- Used memory-mapped file reads by default for fast opens and low RSS.
|
|
53
|
+
- Added direct N-API decoding paths, cached property descriptor names, and an
|
|
54
|
+
IPv4 parser fast path for hot lookups.
|
|
55
|
+
- Added batch lookup and native cursor APIs to reduce JavaScript/native boundary
|
|
56
|
+
crossings.
|
|
57
|
+
|
|
58
|
+
### Development
|
|
59
|
+
|
|
60
|
+
- Added CI for Node 20, 22, and 24, plus macOS and Windows coverage.
|
|
61
|
+
- Added Rust formatting, `cargo check`, clippy, TypeScript, Node test, npm pack,
|
|
62
|
+
and packed-package smoke-test validation.
|
|
63
|
+
|
|
64
|
+
[0.1.0]: https://github.com/oschwald/maxminddb-node/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015, Gregory J. Oschwald <oschwald@gmail.com>
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# @oschwald/maxminddb
|
|
2
|
+
|
|
3
|
+
Rust-backed Node.js reader for MaxMind DB files.
|
|
4
|
+
|
|
5
|
+
The public API is compatible with the commonly used `maxmind` package from
|
|
6
|
+
`node-maxmind` and adds Rust-backed extensions for path lookup, batch lookup,
|
|
7
|
+
and network iteration.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Node.js 20 or newer is required.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install @oschwald/maxminddb
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```js
|
|
20
|
+
const maxmind = require('@oschwald/maxminddb');
|
|
21
|
+
|
|
22
|
+
const reader = await maxmind.open('/path/to/GeoIP2-City.mmdb');
|
|
23
|
+
|
|
24
|
+
console.log(reader.get('8.8.8.8'));
|
|
25
|
+
console.log(reader.getWithPrefixLength('8.8.8.8'));
|
|
26
|
+
console.log(reader.getPath('8.8.8.8', ['country', 'iso_code']));
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Compatibility API
|
|
30
|
+
|
|
31
|
+
- `open(filepath, options?)`
|
|
32
|
+
- `new Reader(buffer, options?)`
|
|
33
|
+
- `reader.get(ipAddress)`
|
|
34
|
+
- `reader.getWithPrefixLength(ipAddress)`
|
|
35
|
+
- `reader.load(buffer)`
|
|
36
|
+
- `reader.metadata`
|
|
37
|
+
- `validate(ipAddress)`
|
|
38
|
+
- `init()` and `openSync()` keep the legacy `node-maxmind` error behavior.
|
|
39
|
+
|
|
40
|
+
`open()` accepts the existing `node-maxmind` options:
|
|
41
|
+
|
|
42
|
+
- `cache`
|
|
43
|
+
- `watchForUpdates`
|
|
44
|
+
- `watchForUpdatesNonPersistent`
|
|
45
|
+
- `watchForUpdatesHook`
|
|
46
|
+
|
|
47
|
+
`cache` controls a native LRU cache of materialized records keyed by MaxMind DB
|
|
48
|
+
data offset. The default is 10,000 records, matching `node-maxmind`. Pass
|
|
49
|
+
`cache: { max: 1000 }` to tune the cache size or `cache: false` to disable it.
|
|
50
|
+
Use `reader.cacheStats()` to inspect hit/miss counters and `reader.clearCache()`
|
|
51
|
+
to release cached record references.
|
|
52
|
+
|
|
53
|
+
The record cache stores JavaScript objects, not compressed database bytes. Large
|
|
54
|
+
cache sizes can therefore retain significant heap memory when the database
|
|
55
|
+
records are large or lookups touch many distinct data offsets. Cached records
|
|
56
|
+
are returned by reference, so mutating a cached record can affect later lookups
|
|
57
|
+
for the same data offset until that entry is evicted, `reader.clearCache()` is
|
|
58
|
+
called, or the reader is closed. `getPath()`, `getManyPath()`, and compiled
|
|
59
|
+
`reader.path()` lookups decode only the requested path and do not populate the
|
|
60
|
+
full-record cache.
|
|
61
|
+
|
|
62
|
+
When `watchForUpdates` is enabled, file-change reloads run serially. A failed
|
|
63
|
+
watched reload leaves the existing reader active, stores the failure on
|
|
64
|
+
`reader.lastReloadError`, and skips `watchForUpdatesHook`. The next successful
|
|
65
|
+
reload clears `lastReloadError` and calls the hook. Close watched readers with
|
|
66
|
+
`reader.close()` to remove the file watcher.
|
|
67
|
+
|
|
68
|
+
## Extensions
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
reader.getPath('8.8.8.8', ['country', 'iso_code']);
|
|
72
|
+
reader.getMany(['8.8.8.8', '1.1.1.1']);
|
|
73
|
+
reader.getManyPath(['8.8.8.8', '1.1.1.1'], ['country', 'iso_code']);
|
|
74
|
+
|
|
75
|
+
const countryCode = reader.path(['country', 'iso_code']);
|
|
76
|
+
countryCode.get('8.8.8.8');
|
|
77
|
+
countryCode.getMany(['8.8.8.8', '1.1.1.1']);
|
|
78
|
+
|
|
79
|
+
for (const [network, record] of reader.within('81.2.69.142/31')) {
|
|
80
|
+
console.log(network, record);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const page of reader.withinPages('81.2.69.0/24', { pageSize: 100 })) {
|
|
84
|
+
for (const [network, record] of page) {
|
|
85
|
+
console.log(network, record);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Path elements are strings for map keys and numbers for array indexes. Negative
|
|
91
|
+
indexes count from the end of an array.
|
|
92
|
+
|
|
93
|
+
Create compiled path lookups once and reuse them in hot paths. `reader.path()`
|
|
94
|
+
parses and stores the path, and the returned `PathLookup` avoids reparsing the
|
|
95
|
+
path array on each lookup.
|
|
96
|
+
|
|
97
|
+
For high-volume lookup workloads, prefer `getMany()` or `getManyPath()` when
|
|
98
|
+
you can batch IPs. They cross the native boundary once for the whole batch and
|
|
99
|
+
are significantly faster than calling `get()` in a JavaScript loop.
|
|
100
|
+
|
|
101
|
+
`networks()` and `within()` return lazy iterators backed by native cursors.
|
|
102
|
+
For large network walks, use `networkPages()`, `withinPages()`, or
|
|
103
|
+
`NetworkIterator#nextPage()` to cross the native boundary once per page rather
|
|
104
|
+
than once per network.
|
|
105
|
+
|
|
106
|
+
## Open Modes
|
|
107
|
+
|
|
108
|
+
Path-based `open()` defaults to memory-mapped reads:
|
|
109
|
+
|
|
110
|
+
- `MODE_AUTO`
|
|
111
|
+
- `MODE_MMAP`
|
|
112
|
+
- `MODE_MEMORY`
|
|
113
|
+
- `MODE_BUFFER`
|
|
114
|
+
|
|
115
|
+
Use `MODE_BUFFER` if you want `open()` to read the file into a Node `Buffer`
|
|
116
|
+
before constructing the reader.
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
const reader = await maxmind.open('/path/to/db.mmdb', {
|
|
120
|
+
mode: maxmind.MODE_MEMORY,
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Mode tradeoffs:
|
|
125
|
+
|
|
126
|
+
- `MODE_MMAP`/`MODE_AUTO` opens quickly and keeps RSS low by mapping the
|
|
127
|
+
database file. Replace database files atomically when using watched reloads.
|
|
128
|
+
- `MODE_MEMORY` reads the database into Rust-owned memory. It costs more memory
|
|
129
|
+
at open time but is independent of the source file after open.
|
|
130
|
+
- `MODE_BUFFER` reads the database into a Node `Buffer` before constructing the
|
|
131
|
+
native reader. Use it when you need Node-side file loading behavior or when
|
|
132
|
+
tests need to mutate a watched temporary file safely.
|
|
133
|
+
|
|
134
|
+
## Performance Notes
|
|
135
|
+
|
|
136
|
+
Performance depends on database size, record shape, cache hit rate, CPU, Node
|
|
137
|
+
version, and whether the database is warm in the OS page cache. On one local
|
|
138
|
+
run with 200,000 generated IPv4 lookups against `/var/lib/GeoIP`, this module
|
|
139
|
+
had much faster open times and lower RSS than `node-maxmind`, while cached
|
|
140
|
+
single-record lookup throughput was still lower:
|
|
141
|
+
|
|
142
|
+
| Database | maxminddb default cache | node-maxmind default cache | maxminddb cache:100k | node-maxmind cache:100k |
|
|
143
|
+
| --- | ---: | ---: | ---: | ---: |
|
|
144
|
+
| GeoIP2-City | 370k/s | 441k/s | 632k/s | 869k/s |
|
|
145
|
+
| GeoLite2-City | 452k/s | 498k/s | 715k/s | 1.07M/s |
|
|
146
|
+
|
|
147
|
+
The same run opened mapped readers in sub-millisecond to low single-digit
|
|
148
|
+
milliseconds after warmup, while `node-maxmind` open times were tens of
|
|
149
|
+
milliseconds and retained tens to hundreds of MB of RSS. Batch lookups were
|
|
150
|
+
faster than JavaScript loops over `get()`, reaching roughly 3.0-3.4M IPs/s in
|
|
151
|
+
that run.
|
|
152
|
+
|
|
153
|
+
Run local benchmarks with:
|
|
154
|
+
|
|
155
|
+
```sh
|
|
156
|
+
npm run bench -- --compare-node-maxmind --db /path/to/db.mmdb
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Supported Platforms
|
|
160
|
+
|
|
161
|
+
The npm package is set up to ship prebuilt native modules for Linux x64 GNU,
|
|
162
|
+
Linux arm64 GNU, macOS x64, macOS arm64, Windows x64 MSVC, and Windows arm64
|
|
163
|
+
MSVC. The loader also knows platform-specific filenames for additional Linux,
|
|
164
|
+
Windows, and FreeBSD targets, but those artifacts are not part of the default
|
|
165
|
+
trusted publishing workflow yet. See [RELEASE.md](./RELEASE.md) for the native
|
|
166
|
+
artifact strategy.
|
|
167
|
+
|
|
168
|
+
## Development
|
|
169
|
+
|
|
170
|
+
```sh
|
|
171
|
+
npm install
|
|
172
|
+
npm run build
|
|
173
|
+
npm test
|
|
174
|
+
npm run typecheck
|
|
175
|
+
npm run bench -- --compare-node-maxmind
|
|
176
|
+
npm run release
|
|
177
|
+
npm run bench -- --save-baseline /tmp/maxminddb-baseline.json
|
|
178
|
+
npm run bench -- --baseline /tmp/maxminddb-baseline.json --min-ratio 0.9
|
|
179
|
+
npm run --silent bench -- --json > bench-results.json
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
See [RELEASE.md](./RELEASE.md) for packaging expectations and the native
|
|
183
|
+
prebuild release strategy.
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
ISC License. See [LICENSE](./LICENSE) for details.
|
package/RELEASE.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Release Notes
|
|
2
|
+
|
|
3
|
+
This package loads a native N-API addon at runtime. A publishable npm tarball
|
|
4
|
+
must therefore include built `.node` files for the platforms it supports.
|
|
5
|
+
|
|
6
|
+
## Current Artifact Shape
|
|
7
|
+
|
|
8
|
+
`index.js` probes for platform-specific native bindings first, then falls back
|
|
9
|
+
to `index.node`:
|
|
10
|
+
|
|
11
|
+
- Linux: `index.linux-*-*.node`
|
|
12
|
+
- macOS: `index.darwin-*.node`
|
|
13
|
+
- Windows: `index.win32-*-*.node`
|
|
14
|
+
- FreeBSD: `index.freebsd-x64.node`
|
|
15
|
+
- Generic fallback: `index.node`
|
|
16
|
+
|
|
17
|
+
`package.json` includes `*.node` files in the tarball, but it does not build a
|
|
18
|
+
native addon during package installation. For local packaging checks, build the
|
|
19
|
+
addon before packing:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm ci
|
|
23
|
+
npm run build
|
|
24
|
+
npm pack --dry-run
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The dry run should show `index.js`, `index.d.ts`, `package.json`, `README.md`,
|
|
28
|
+
and one or more `.node` files.
|
|
29
|
+
|
|
30
|
+
The trusted publishing workflow assembles one npm package containing these
|
|
31
|
+
prebuilt native modules:
|
|
32
|
+
|
|
33
|
+
- Linux x64 GNU: `index.linux-x64-gnu.node`
|
|
34
|
+
- Linux arm64 GNU: `index.linux-arm64-gnu.node`
|
|
35
|
+
- macOS x64: `index.darwin-x64.node`
|
|
36
|
+
- macOS arm64: `index.darwin-arm64.node`
|
|
37
|
+
- Windows x64 MSVC: `index.win32-x64-msvc.node`
|
|
38
|
+
- Windows arm64 MSVC: `index.win32-arm64-msvc.node`
|
|
39
|
+
|
|
40
|
+
The workflow does not currently build Linux musl/Alpine, Linux armv7, Linux
|
|
41
|
+
ppc64/s390x/riscv64, Windows ia32, or FreeBSD artifacts. The loader supports
|
|
42
|
+
platform-specific filenames for several of those targets, so adding them is a
|
|
43
|
+
release automation task rather than a runtime API change.
|
|
44
|
+
|
|
45
|
+
## Publishing Strategy
|
|
46
|
+
|
|
47
|
+
The repository currently publishes `@oschwald/maxminddb` as one package
|
|
48
|
+
containing every supported `index.<triple>.node` file. This keeps installation
|
|
49
|
+
simple and avoids install-time native builds. If the tarball becomes too large,
|
|
50
|
+
the next packaging shape to consider is separate optional dependency packages
|
|
51
|
+
per target triple:
|
|
52
|
+
|
|
53
|
+
- Publish separate optional dependency packages per target triple, each
|
|
54
|
+
containing one `index.<triple>.node`.
|
|
55
|
+
|
|
56
|
+
That model keeps install size small while preserving install-time reliability.
|
|
57
|
+
Publishing source only and building during package installation is intentionally
|
|
58
|
+
not the default release strategy.
|
|
59
|
+
|
|
60
|
+
The package is scoped and public. `package.json` sets `publishConfig.access` to
|
|
61
|
+
`public`, and local bootstrap publishes should also pass `--access public`.
|
|
62
|
+
|
|
63
|
+
## Trusted Publishing
|
|
64
|
+
|
|
65
|
+
Publishing is configured through `.github/workflows/publish.yml` and npm trusted
|
|
66
|
+
publishing. The workflow does not use an `NPM_TOKEN`; it grants GitHub Actions
|
|
67
|
+
`id-token: write` permission so npm can authenticate the publish through OIDC.
|
|
68
|
+
|
|
69
|
+
Before the first publish, configure the package on npmjs.com:
|
|
70
|
+
|
|
71
|
+
- Publisher: GitHub Actions
|
|
72
|
+
- Organization or user: the GitHub owner of this repository
|
|
73
|
+
- Repository: the GitHub repository name
|
|
74
|
+
- Workflow filename: `publish.yml`
|
|
75
|
+
- Allowed action: `npm publish`
|
|
76
|
+
|
|
77
|
+
The workflow uses Node 24 and upgrades npm before publishing so the npm CLI
|
|
78
|
+
meets trusted publishing requirements. It builds native modules on hosted
|
|
79
|
+
Linux, macOS, and Windows runners, smoke-tests that each native module loads on
|
|
80
|
+
its build runner, downloads those artifacts into the package root, runs the same
|
|
81
|
+
validation as CI, and verifies the package with `npm pack --dry-run`. GitHub
|
|
82
|
+
release events then run `npm publish`. Manual `workflow_dispatch` runs are
|
|
83
|
+
validation-only unless the `publish` input is set to `true`.
|
|
84
|
+
|
|
85
|
+
## Release Checklist
|
|
86
|
+
|
|
87
|
+
1. Configure npm trusted publishing for `.github/workflows/publish.yml`.
|
|
88
|
+
2. Add a top entry to `CHANGELOG.md` with today's date.
|
|
89
|
+
3. Run `npm run release`.
|
|
90
|
+
4. Run `npm run bench -- --compare-node-maxmind` when benchmark databases are
|
|
91
|
+
available.
|
|
92
|
+
5. Verify the `build-binaries` matrix completed for every supported target
|
|
93
|
+
before the `publish` job runs `npm publish`.
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
export type Names = {
|
|
2
|
+
readonly de?: string;
|
|
3
|
+
readonly en: string;
|
|
4
|
+
readonly es?: string;
|
|
5
|
+
readonly fr?: string;
|
|
6
|
+
readonly ja?: string;
|
|
7
|
+
readonly 'pt-BR'?: string;
|
|
8
|
+
readonly ru?: string;
|
|
9
|
+
readonly 'zh-CN'?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export interface CityRecord {
|
|
13
|
+
readonly confidence?: number;
|
|
14
|
+
readonly geoname_id: number;
|
|
15
|
+
readonly names: Names;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ContinentRecord {
|
|
19
|
+
readonly code: 'AF' | 'AN' | 'AS' | 'EU' | 'NA' | 'OC' | 'SA';
|
|
20
|
+
readonly geoname_id: number;
|
|
21
|
+
readonly names: Names;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RegisteredCountryRecord {
|
|
25
|
+
readonly geoname_id: number;
|
|
26
|
+
readonly is_in_european_union?: boolean;
|
|
27
|
+
readonly iso_code: string;
|
|
28
|
+
readonly names: Names;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CountryRecord extends RegisteredCountryRecord {
|
|
32
|
+
readonly confidence?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface LocationRecord {
|
|
36
|
+
readonly accuracy_radius: number;
|
|
37
|
+
readonly average_income?: number;
|
|
38
|
+
readonly latitude: number;
|
|
39
|
+
readonly longitude: number;
|
|
40
|
+
readonly metro_code?: number;
|
|
41
|
+
readonly population_density?: number;
|
|
42
|
+
readonly time_zone?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PostalRecord {
|
|
46
|
+
readonly code: string;
|
|
47
|
+
readonly confidence?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RepresentedCountryRecord extends RegisteredCountryRecord {
|
|
51
|
+
readonly type: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface SubdivisionsRecord {
|
|
55
|
+
readonly confidence?: number;
|
|
56
|
+
readonly geoname_id: number;
|
|
57
|
+
readonly iso_code: string;
|
|
58
|
+
readonly names: Names;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface TraitsRecord {
|
|
62
|
+
readonly autonomous_system_number?: number;
|
|
63
|
+
readonly autonomous_system_organization?: string;
|
|
64
|
+
readonly connection_type?: string;
|
|
65
|
+
readonly domain?: string;
|
|
66
|
+
ip_address?: string;
|
|
67
|
+
readonly is_anonymous?: boolean;
|
|
68
|
+
readonly is_anonymous_proxy?: boolean;
|
|
69
|
+
readonly is_anonymous_vpn?: boolean;
|
|
70
|
+
readonly is_anycast?: boolean;
|
|
71
|
+
readonly is_hosting_provider?: boolean;
|
|
72
|
+
readonly is_legitimate_proxy?: boolean;
|
|
73
|
+
readonly is_public_proxy?: boolean;
|
|
74
|
+
readonly is_residential_proxy?: boolean;
|
|
75
|
+
readonly is_satellite_provider?: boolean;
|
|
76
|
+
readonly is_tor_exit_node?: boolean;
|
|
77
|
+
readonly isp?: string;
|
|
78
|
+
readonly mobile_country_code?: string;
|
|
79
|
+
readonly mobile_network_code?: string;
|
|
80
|
+
readonly organization?: string;
|
|
81
|
+
readonly static_ip_score?: number;
|
|
82
|
+
readonly user_count?: number;
|
|
83
|
+
readonly user_type?: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface CountryResponse {
|
|
87
|
+
readonly continent?: ContinentRecord;
|
|
88
|
+
readonly country?: CountryRecord;
|
|
89
|
+
readonly registered_country?: RegisteredCountryRecord;
|
|
90
|
+
readonly represented_country?: RepresentedCountryRecord;
|
|
91
|
+
readonly traits?: TraitsRecord;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface CityResponse extends CountryResponse {
|
|
95
|
+
readonly city?: CityRecord;
|
|
96
|
+
readonly location?: LocationRecord;
|
|
97
|
+
readonly postal?: PostalRecord;
|
|
98
|
+
readonly subdivisions?: SubdivisionsRecord[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface AnonymousIPResponse {
|
|
102
|
+
ip_address?: string;
|
|
103
|
+
readonly is_anonymous?: boolean;
|
|
104
|
+
readonly is_anonymous_proxy?: boolean;
|
|
105
|
+
readonly is_anonymous_vpn?: boolean;
|
|
106
|
+
readonly is_hosting_provider?: boolean;
|
|
107
|
+
readonly is_public_proxy?: boolean;
|
|
108
|
+
readonly is_residential_proxy?: boolean;
|
|
109
|
+
readonly is_tor_exit_node?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface AnonymousPlusResponse extends AnonymousIPResponse {
|
|
113
|
+
readonly anonymizer_confidence?: number;
|
|
114
|
+
readonly network_last_seen?: string;
|
|
115
|
+
readonly provider_name?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface AsnResponse {
|
|
119
|
+
readonly autonomous_system_number: number;
|
|
120
|
+
readonly autonomous_system_organization: string;
|
|
121
|
+
ip_address?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ConnectionTypeResponse {
|
|
125
|
+
readonly connection_type: string;
|
|
126
|
+
ip_address?: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface DomainResponse {
|
|
130
|
+
readonly domain: string;
|
|
131
|
+
ip_address?: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface IspResponse extends AsnResponse {
|
|
135
|
+
readonly isp: string;
|
|
136
|
+
readonly mobile_country_code?: string;
|
|
137
|
+
readonly mobile_network_code?: string;
|
|
138
|
+
readonly organization: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export type Response =
|
|
142
|
+
| CountryResponse
|
|
143
|
+
| CityResponse
|
|
144
|
+
| AnonymousIPResponse
|
|
145
|
+
| AnonymousPlusResponse
|
|
146
|
+
| AsnResponse
|
|
147
|
+
| ConnectionTypeResponse
|
|
148
|
+
| DomainResponse
|
|
149
|
+
| IspResponse;
|
|
150
|
+
|
|
151
|
+
export interface Metadata {
|
|
152
|
+
readonly binaryFormatMajorVersion: number;
|
|
153
|
+
readonly binaryFormatMinorVersion: number;
|
|
154
|
+
readonly buildEpoch: Date;
|
|
155
|
+
readonly databaseType: string;
|
|
156
|
+
readonly languages: string[];
|
|
157
|
+
readonly description: Record<string, string>;
|
|
158
|
+
readonly ipVersion: number;
|
|
159
|
+
readonly nodeCount: number;
|
|
160
|
+
readonly recordSize: number;
|
|
161
|
+
readonly nodeByteSize: number;
|
|
162
|
+
readonly searchTreeSize: number;
|
|
163
|
+
readonly treeDepth: number;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface OpenOpts {
|
|
167
|
+
cache?: false | {
|
|
168
|
+
max: number;
|
|
169
|
+
};
|
|
170
|
+
watchForUpdates?: boolean;
|
|
171
|
+
watchForUpdatesNonPersistent?: boolean;
|
|
172
|
+
watchForUpdatesHook?: () => void;
|
|
173
|
+
mode?: typeof MODE_AUTO | typeof MODE_MMAP | typeof MODE_MEMORY | typeof MODE_BUFFER;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface NetworkIterationOptions {
|
|
177
|
+
includeAliasedNetworks?: boolean;
|
|
178
|
+
includeNetworksWithoutData?: boolean;
|
|
179
|
+
skipEmptyValues?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface NetworkPagesOptions extends NetworkIterationOptions {
|
|
183
|
+
pageSize?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export interface CacheStats {
|
|
187
|
+
readonly enabled: boolean;
|
|
188
|
+
readonly size: number;
|
|
189
|
+
readonly capacity: number;
|
|
190
|
+
readonly hits: number;
|
|
191
|
+
readonly misses: number;
|
|
192
|
+
readonly inserts: number;
|
|
193
|
+
readonly evictions: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export declare class PathLookup<TValue = unknown> {
|
|
197
|
+
readonly path: ReadonlyArray<string | number>;
|
|
198
|
+
get(ipAddress: string): TValue | null;
|
|
199
|
+
getMany(ipAddresses: ReadonlyArray<string>): Array<TValue | null>;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export declare class NetworkIterator<T extends Response = Response>
|
|
203
|
+
implements IterableIterator<[string, T | null]> {
|
|
204
|
+
next(): IteratorResult<[string, T | null]>;
|
|
205
|
+
nextPage(pageSize?: number): Array<[string, T | null]>;
|
|
206
|
+
pages(pageSize?: number): IterableIterator<Array<[string, T | null]>>;
|
|
207
|
+
close(): void;
|
|
208
|
+
[Symbol.iterator](): IterableIterator<[string, T | null]>;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export declare const MODE_AUTO: 'auto';
|
|
212
|
+
export declare const MODE_MMAP: 'mmap';
|
|
213
|
+
export declare const MODE_MEMORY: 'memory';
|
|
214
|
+
export declare const MODE_BUFFER: 'buffer';
|
|
215
|
+
|
|
216
|
+
export declare class Reader<T extends Response = Response> {
|
|
217
|
+
constructor(database: Buffer, options?: OpenOpts);
|
|
218
|
+
readonly closed: boolean;
|
|
219
|
+
readonly lastReloadError: Error | null;
|
|
220
|
+
metadata: Metadata;
|
|
221
|
+
load(database: Buffer): void;
|
|
222
|
+
reload(): void;
|
|
223
|
+
close(): void;
|
|
224
|
+
clearCache(): void;
|
|
225
|
+
cacheStats(): CacheStats;
|
|
226
|
+
get(ipAddress: string): T | null;
|
|
227
|
+
getPath(ipAddress: string, path: ReadonlyArray<string | number>): unknown;
|
|
228
|
+
path<TValue = unknown>(
|
|
229
|
+
path: ReadonlyArray<string | number>,
|
|
230
|
+
): PathLookup<TValue>;
|
|
231
|
+
getWithPrefixLength(ipAddress: string): [T | null, number];
|
|
232
|
+
getMany(ipAddresses: ReadonlyArray<string>): Array<T | null>;
|
|
233
|
+
getManyPath(
|
|
234
|
+
ipAddresses: ReadonlyArray<string>,
|
|
235
|
+
path: ReadonlyArray<string | number>,
|
|
236
|
+
): unknown[];
|
|
237
|
+
networks(options?: NetworkPagesOptions): NetworkIterator<T>;
|
|
238
|
+
within(cidr: string, options?: NetworkPagesOptions): NetworkIterator<T>;
|
|
239
|
+
networkPages(
|
|
240
|
+
options?: NetworkPagesOptions,
|
|
241
|
+
): IterableIterator<Array<[string, T | null]>>;
|
|
242
|
+
withinPages(
|
|
243
|
+
cidr: string,
|
|
244
|
+
options?: NetworkPagesOptions,
|
|
245
|
+
): IterableIterator<Array<[string, T | null]>>;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export declare function open<T extends Response = Response>(
|
|
249
|
+
filepath: string,
|
|
250
|
+
options?: OpenOpts,
|
|
251
|
+
): Promise<Reader<T>>;
|
|
252
|
+
|
|
253
|
+
export declare function openSync(): never;
|
|
254
|
+
export declare function init(): never;
|
|
255
|
+
export declare function validate(ipAddress: string): boolean;
|
|
256
|
+
export declare function nativeVersion(): string;
|
|
257
|
+
|
|
258
|
+
declare const maxmind: {
|
|
259
|
+
PathLookup: typeof PathLookup;
|
|
260
|
+
Reader: typeof Reader;
|
|
261
|
+
open: typeof open;
|
|
262
|
+
openSync: typeof openSync;
|
|
263
|
+
init: typeof init;
|
|
264
|
+
validate: typeof validate;
|
|
265
|
+
nativeVersion: typeof nativeVersion;
|
|
266
|
+
MODE_AUTO: typeof MODE_AUTO;
|
|
267
|
+
MODE_MMAP: typeof MODE_MMAP;
|
|
268
|
+
MODE_MEMORY: typeof MODE_MEMORY;
|
|
269
|
+
MODE_BUFFER: typeof MODE_BUFFER;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
export default maxmind;
|
|
Binary file
|
|
Binary file
|
package/index.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const net = require('node:net');
|
|
6
|
+
const { join } = require('node:path');
|
|
7
|
+
|
|
8
|
+
const platformTriples = {
|
|
9
|
+
linux: {
|
|
10
|
+
x64: ['linux-x64-gnu', 'linux-x64-musl'],
|
|
11
|
+
arm64: ['linux-arm64-gnu', 'linux-arm64-musl'],
|
|
12
|
+
arm: ['linux-arm-gnueabihf', 'linux-arm-musleabihf'],
|
|
13
|
+
ppc64: ['linux-ppc64-gnu'],
|
|
14
|
+
s390x: ['linux-s390x-gnu'],
|
|
15
|
+
riscv64: ['linux-riscv64-gnu'],
|
|
16
|
+
},
|
|
17
|
+
darwin: {
|
|
18
|
+
x64: ['darwin-x64'],
|
|
19
|
+
arm64: ['darwin-arm64'],
|
|
20
|
+
universal: ['darwin-universal'],
|
|
21
|
+
},
|
|
22
|
+
win32: {
|
|
23
|
+
x64: ['win32-x64-msvc', 'win32-x64-gnu'],
|
|
24
|
+
arm64: ['win32-arm64-msvc'],
|
|
25
|
+
ia32: ['win32-ia32-msvc'],
|
|
26
|
+
},
|
|
27
|
+
freebsd: {
|
|
28
|
+
x64: ['freebsd-x64'],
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function nativeCandidates() {
|
|
33
|
+
const triples = platformTriples[process.platform]?.[process.arch] ?? [];
|
|
34
|
+
return [
|
|
35
|
+
...triples.map((triple) => `index.${triple}.node`),
|
|
36
|
+
'index.node',
|
|
37
|
+
].map((name) => join(__dirname, name));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadNativeBinding() {
|
|
41
|
+
const attempted = [];
|
|
42
|
+
for (const candidate of nativeCandidates()) {
|
|
43
|
+
attempted.push(candidate);
|
|
44
|
+
if (fs.existsSync(candidate)) {
|
|
45
|
+
return require(candidate);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const error = new Error(
|
|
50
|
+
`Unable to load maxminddb native binding. Tried:\n${attempted.join('\n')}`
|
|
51
|
+
);
|
|
52
|
+
error.code = 'ERR_MAXMINDDB_NATIVE_BINDING_NOT_FOUND';
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const native = loadNativeBinding();
|
|
57
|
+
|
|
58
|
+
const DEFAULT_LARGE_FILE_THRESHOLD = 512 * 1024 * 1024;
|
|
59
|
+
const LARGE_FILE_THRESHOLD = normalizeLargeFileThreshold(
|
|
60
|
+
process.env.MAXMINDDB_LARGE_FILE_THRESHOLD_BYTES
|
|
61
|
+
);
|
|
62
|
+
const STREAM_WATERMARK = 8 * 1024 * 1024;
|
|
63
|
+
const DEFAULT_CACHE_MAX = 10_000;
|
|
64
|
+
const MAX_CACHE_MAX = 0xffffffff;
|
|
65
|
+
const legacyErrorMessage = `Maxmind v2 module has changed API.
|
|
66
|
+
Please use:
|
|
67
|
+
maxmind.open(dbfile).then(function(lookup) {
|
|
68
|
+
lookup.get(ip);
|
|
69
|
+
});
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const MODE_AUTO = 'auto';
|
|
73
|
+
const MODE_MMAP = 'mmap';
|
|
74
|
+
const MODE_MEMORY = 'memory';
|
|
75
|
+
const MODE_BUFFER = 'buffer';
|
|
76
|
+
|
|
77
|
+
function normalizeLargeFileThreshold(value) {
|
|
78
|
+
if (value == null || value === '') {
|
|
79
|
+
return DEFAULT_LARGE_FILE_THRESHOLD;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const threshold = Number(value);
|
|
83
|
+
return Number.isSafeInteger(threshold) && threshold >= 0
|
|
84
|
+
? threshold
|
|
85
|
+
: DEFAULT_LARGE_FILE_THRESHOLD;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isGzipBuffer(buffer) {
|
|
89
|
+
return buffer.length >= 2 && buffer[0] === 0x1f && buffer[1] === 0x8b;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function assertNotGzipFile(filepath) {
|
|
93
|
+
const handle = await fs.promises.open(filepath, 'r');
|
|
94
|
+
try {
|
|
95
|
+
const buffer = Buffer.alloc(2);
|
|
96
|
+
const { bytesRead } = await handle.read(buffer, 0, 2, 0);
|
|
97
|
+
if (bytesRead === 2 && isGzipBuffer(buffer)) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'Looks like you are passing in a file in gzip format, please use mmdb database instead.'
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
} finally {
|
|
103
|
+
await handle.close();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeMode(mode) {
|
|
108
|
+
if (mode == null || mode === MODE_AUTO) {
|
|
109
|
+
return MODE_MMAP;
|
|
110
|
+
}
|
|
111
|
+
if (mode === MODE_MMAP || mode === MODE_MEMORY || mode === MODE_BUFFER) {
|
|
112
|
+
return mode;
|
|
113
|
+
}
|
|
114
|
+
throw new Error(`Unsupported open mode: ${mode}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeMetadata(metadata) {
|
|
118
|
+
return {
|
|
119
|
+
...metadata,
|
|
120
|
+
buildEpoch: new Date(Number(metadata.buildEpoch) * 1000),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function normalizeCacheCapacity(options = {}) {
|
|
125
|
+
if (options.cache === false) {
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const max = options.cache?.max ?? DEFAULT_CACHE_MAX;
|
|
130
|
+
if (!Number.isSafeInteger(max) || max <= 0 || max > MAX_CACHE_MAX) {
|
|
131
|
+
throw new Error('opts.cache.max should be a positive 32-bit integer');
|
|
132
|
+
}
|
|
133
|
+
return max;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeNetworkOptions(options = {}) {
|
|
137
|
+
return [
|
|
138
|
+
Boolean(options.includeAliasedNetworks),
|
|
139
|
+
Boolean(options.includeNetworksWithoutData),
|
|
140
|
+
Boolean(options.skipEmptyValues),
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeNetworkPageSize(value = 1000) {
|
|
145
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > MAX_CACHE_MAX) {
|
|
146
|
+
throw new Error('page size should be a positive 32-bit integer');
|
|
147
|
+
}
|
|
148
|
+
return value;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function normalizeNetworkIteratorOptions(options = {}) {
|
|
152
|
+
return [
|
|
153
|
+
normalizeNetworkOptions(options),
|
|
154
|
+
normalizeNetworkPageSize(options.pageSize ?? 1000),
|
|
155
|
+
];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function waitForFile(filepath) {
|
|
159
|
+
for (let i = 0; i < 3; i++) {
|
|
160
|
+
if (fs.existsSync(filepath)) {
|
|
161
|
+
return Promise.resolve(true);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Promise((resolve) => {
|
|
166
|
+
let attempts = 0;
|
|
167
|
+
const retry = () => {
|
|
168
|
+
attempts += 1;
|
|
169
|
+
if (fs.existsSync(filepath)) {
|
|
170
|
+
resolve(true);
|
|
171
|
+
} else if (attempts >= 3) {
|
|
172
|
+
resolve(false);
|
|
173
|
+
} else {
|
|
174
|
+
setTimeout(retry, 500);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
retry();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function readLargeFile(filepath, size) {
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const buffer = Buffer.allocUnsafe(size);
|
|
184
|
+
let offset = 0;
|
|
185
|
+
let settled = false;
|
|
186
|
+
const stream = fs.createReadStream(filepath, {
|
|
187
|
+
highWaterMark: STREAM_WATERMARK,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const finish = (error, value) => {
|
|
191
|
+
if (settled) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
settled = true;
|
|
195
|
+
if (error) {
|
|
196
|
+
stream.destroy();
|
|
197
|
+
reject(error);
|
|
198
|
+
} else {
|
|
199
|
+
resolve(value);
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
stream.on('data', (chunk) => {
|
|
204
|
+
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
205
|
+
if (offset + bufferChunk.length > size) {
|
|
206
|
+
finish(
|
|
207
|
+
new Error(
|
|
208
|
+
`File changed while reading ${filepath}: expected ${size} bytes but read more`
|
|
209
|
+
)
|
|
210
|
+
);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
bufferChunk.copy(buffer, offset);
|
|
214
|
+
offset += bufferChunk.length;
|
|
215
|
+
});
|
|
216
|
+
stream.on('end', () => {
|
|
217
|
+
if (offset !== size) {
|
|
218
|
+
finish(
|
|
219
|
+
new Error(
|
|
220
|
+
`File changed while reading ${filepath}: expected ${size} bytes but read ${offset}`
|
|
221
|
+
)
|
|
222
|
+
);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
finish(null, buffer);
|
|
226
|
+
});
|
|
227
|
+
stream.on('error', (error) => finish(error));
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function readFile(filepath) {
|
|
232
|
+
const stat = await fs.promises.stat(filepath);
|
|
233
|
+
return stat.size < LARGE_FILE_THRESHOLD
|
|
234
|
+
? fs.promises.readFile(filepath)
|
|
235
|
+
: readLargeFile(filepath, stat.size);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
class PathLookup {
|
|
239
|
+
constructor(reader, path) {
|
|
240
|
+
this._reader = reader;
|
|
241
|
+
this._pathId = reader._reader.compilePath(path);
|
|
242
|
+
this.path = Object.freeze([...path]);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
get(ipAddress) {
|
|
246
|
+
return this._reader._reader.getCompiledPath(ipAddress, this._pathId);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
getMany(ipAddresses) {
|
|
250
|
+
return this._reader._reader.getManyCompiledPath(ipAddresses, this._pathId);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
class NetworkIterator {
|
|
255
|
+
constructor(reader, cidr, options = {}) {
|
|
256
|
+
const [networkOptions, pageSize] = normalizeNetworkIteratorOptions(options);
|
|
257
|
+
this._cursor = reader._reader.networkCursor(cidr, ...networkOptions);
|
|
258
|
+
this._pageSize = pageSize;
|
|
259
|
+
this._page = [];
|
|
260
|
+
this._index = 0;
|
|
261
|
+
this._done = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
[Symbol.iterator]() {
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
next() {
|
|
269
|
+
if (this._done) {
|
|
270
|
+
return { done: true, value: undefined };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (this._index >= this._page.length) {
|
|
274
|
+
this._page = this._cursor.nextPage(this._pageSize);
|
|
275
|
+
this._index = 0;
|
|
276
|
+
if (this._page.length === 0) {
|
|
277
|
+
this.close();
|
|
278
|
+
return { done: true, value: undefined };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const value = this._page[this._index];
|
|
283
|
+
this._index += 1;
|
|
284
|
+
return { done: false, value };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
nextPage(pageSize = this._pageSize) {
|
|
288
|
+
pageSize = normalizeNetworkPageSize(pageSize);
|
|
289
|
+
if (this._done) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const page = [];
|
|
294
|
+
while (page.length < pageSize && this._index < this._page.length) {
|
|
295
|
+
page.push(this._page[this._index]);
|
|
296
|
+
this._index += 1;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (page.length < pageSize) {
|
|
300
|
+
const nativePage = this._cursor.nextPage(pageSize - page.length);
|
|
301
|
+
page.push(...nativePage);
|
|
302
|
+
if (nativePage.length === 0) {
|
|
303
|
+
this.close();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return page;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
*pages(pageSize = this._pageSize) {
|
|
311
|
+
pageSize = normalizeNetworkPageSize(pageSize);
|
|
312
|
+
while (true) {
|
|
313
|
+
const page = this.nextPage(pageSize);
|
|
314
|
+
if (page.length === 0) {
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
yield page;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
close() {
|
|
322
|
+
if (!this._done) {
|
|
323
|
+
this._cursor.close();
|
|
324
|
+
this._done = true;
|
|
325
|
+
this._page = [];
|
|
326
|
+
this._index = 0;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
class Reader {
|
|
332
|
+
constructor(database, options = {}) {
|
|
333
|
+
if (!Buffer.isBuffer(database)) {
|
|
334
|
+
throw new Error(`maxminddb expects an instance of Buffer, got: ${typeof database}`);
|
|
335
|
+
}
|
|
336
|
+
this._mode = MODE_BUFFER;
|
|
337
|
+
this._filepath = null;
|
|
338
|
+
this._watchFilepath = null;
|
|
339
|
+
this._watchListener = null;
|
|
340
|
+
this._watchReloadPromise = Promise.resolve();
|
|
341
|
+
this._lastReloadError = null;
|
|
342
|
+
this._cacheCapacity = normalizeCacheCapacity(options);
|
|
343
|
+
this._reader = new native.NativeReader(database, this._cacheCapacity);
|
|
344
|
+
this.metadata = normalizeMetadata(this._reader.metadata());
|
|
345
|
+
this.options = options;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
static open(filepath, options = {}) {
|
|
349
|
+
const mode = normalizeMode(options.mode);
|
|
350
|
+
const reader = Object.create(Reader.prototype);
|
|
351
|
+
reader._mode = mode;
|
|
352
|
+
reader._filepath = filepath;
|
|
353
|
+
reader._watchFilepath = null;
|
|
354
|
+
reader._watchListener = null;
|
|
355
|
+
reader._watchReloadPromise = Promise.resolve();
|
|
356
|
+
reader._lastReloadError = null;
|
|
357
|
+
reader._cacheCapacity = normalizeCacheCapacity(options);
|
|
358
|
+
reader._reader = native.openReader(filepath, mode, reader._cacheCapacity);
|
|
359
|
+
reader.metadata = normalizeMetadata(reader._reader.metadata());
|
|
360
|
+
reader.options = options;
|
|
361
|
+
return reader;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
get closed() {
|
|
365
|
+
return this._reader.closed;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
get lastReloadError() {
|
|
369
|
+
return this._lastReloadError;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
load(database) {
|
|
373
|
+
try {
|
|
374
|
+
this._reader.load(database);
|
|
375
|
+
this.metadata = normalizeMetadata(this._reader.metadata());
|
|
376
|
+
this._lastReloadError = null;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
this._lastReloadError = error;
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
reload() {
|
|
384
|
+
if (!this._filepath) {
|
|
385
|
+
throw new Error('Cannot reload a buffer-backed Reader');
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
this._reader.reloadFromFile(this._filepath, this._mode);
|
|
389
|
+
this.metadata = normalizeMetadata(this._reader.metadata());
|
|
390
|
+
this._lastReloadError = null;
|
|
391
|
+
} catch (error) {
|
|
392
|
+
this._lastReloadError = error;
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
close() {
|
|
398
|
+
if (this._watchFilepath && this._watchListener) {
|
|
399
|
+
fs.unwatchFile(this._watchFilepath, this._watchListener);
|
|
400
|
+
this._watchFilepath = null;
|
|
401
|
+
this._watchListener = null;
|
|
402
|
+
}
|
|
403
|
+
this._reader.close();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
_queueWatchedReload(filepath, mode, hook) {
|
|
407
|
+
const reload = () => this._reloadWatchedFile(filepath, mode, hook);
|
|
408
|
+
this._watchReloadPromise = this._watchReloadPromise.then(reload, reload);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async _reloadWatchedFile(filepath, mode, hook) {
|
|
412
|
+
if (!(await waitForFile(filepath))) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (this.closed || this._watchFilepath !== filepath) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
if (mode === MODE_BUFFER) {
|
|
421
|
+
const database = await readFile(filepath);
|
|
422
|
+
if (this.closed || this._watchFilepath !== filepath) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
this.load(database);
|
|
426
|
+
} else {
|
|
427
|
+
this.reload();
|
|
428
|
+
}
|
|
429
|
+
if (!this.closed && this._watchFilepath === filepath && hook) {
|
|
430
|
+
hook();
|
|
431
|
+
}
|
|
432
|
+
} catch (error) {
|
|
433
|
+
this._lastReloadError = error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
clearCache() {
|
|
438
|
+
this._reader.clearCache();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
cacheStats() {
|
|
442
|
+
return this._reader.cacheStats();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
get(ipAddress) {
|
|
446
|
+
return this._reader.get(ipAddress);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
getPath(ipAddress, path) {
|
|
450
|
+
return this._reader.getPath(ipAddress, path);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
path(path) {
|
|
454
|
+
return new PathLookup(this, path);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
getWithPrefixLength(ipAddress) {
|
|
458
|
+
return this._reader.getWithPrefixLength(ipAddress);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
getMany(ipAddresses) {
|
|
462
|
+
return this._reader.getMany(ipAddresses);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
getManyPath(ipAddresses, path) {
|
|
466
|
+
return this._reader.getManyPath(ipAddresses, path);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
networks(options = {}) {
|
|
470
|
+
return new NetworkIterator(this, null, options);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
within(cidr, options = {}) {
|
|
474
|
+
return new NetworkIterator(this, cidr, options);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
*networkPages(options = {}) {
|
|
478
|
+
yield* this.networks(options).pages(options.pageSize ?? 1000);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
*withinPages(cidr, options = {}) {
|
|
482
|
+
yield* this.within(cidr, options).pages(options.pageSize ?? 1000);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function open(filepath, opts, cb) {
|
|
487
|
+
assert(!cb, legacyErrorMessage);
|
|
488
|
+
const options = opts || {};
|
|
489
|
+
await assertNotGzipFile(filepath);
|
|
490
|
+
|
|
491
|
+
const mode = normalizeMode(options.mode);
|
|
492
|
+
const reader =
|
|
493
|
+
mode === MODE_BUFFER
|
|
494
|
+
? new Reader(await readFile(filepath), options)
|
|
495
|
+
: Reader.open(filepath, options);
|
|
496
|
+
|
|
497
|
+
if (options.watchForUpdates) {
|
|
498
|
+
if (
|
|
499
|
+
options.watchForUpdatesHook &&
|
|
500
|
+
typeof options.watchForUpdatesHook !== 'function'
|
|
501
|
+
) {
|
|
502
|
+
throw new Error('opts.watchForUpdatesHook should be a function');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const watcherOptions = {
|
|
506
|
+
persistent: options.watchForUpdatesNonPersistent !== true,
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const watchListener = () => {
|
|
510
|
+
reader._queueWatchedReload(filepath, mode, options.watchForUpdatesHook);
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
fs.watchFile(filepath, watcherOptions, watchListener);
|
|
514
|
+
reader._watchFilepath = filepath;
|
|
515
|
+
reader._watchListener = watchListener;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return reader;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function init() {
|
|
522
|
+
throw new Error(legacyErrorMessage);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function openSync() {
|
|
526
|
+
throw new Error(legacyErrorMessage);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function validate(ipAddress) {
|
|
530
|
+
const version = net.isIP(ipAddress);
|
|
531
|
+
return version === 4 || version === 6;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
module.exports = {
|
|
535
|
+
...native,
|
|
536
|
+
NetworkIterator,
|
|
537
|
+
PathLookup,
|
|
538
|
+
Reader,
|
|
539
|
+
init,
|
|
540
|
+
open,
|
|
541
|
+
openSync,
|
|
542
|
+
validate,
|
|
543
|
+
MODE_AUTO,
|
|
544
|
+
MODE_MMAP,
|
|
545
|
+
MODE_MEMORY,
|
|
546
|
+
MODE_BUFFER,
|
|
547
|
+
};
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oschwald/maxminddb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Rust-backed Node.js reader for MaxMind DB files",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./index.d.ts",
|
|
10
|
+
"require": "./index.js",
|
|
11
|
+
"default": "./index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.js",
|
|
17
|
+
"index.d.ts",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"LICENSE",
|
|
20
|
+
"RELEASE.md",
|
|
21
|
+
"*.node"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/oschwald/maxminddb-node.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/oschwald/maxminddb-node/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/oschwald/maxminddb-node#readme",
|
|
31
|
+
"author": "Gregory J. Oschwald <oschwald@gmail.com>",
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"registry": "https://registry.npmjs.org/",
|
|
35
|
+
"provenance": true
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "napi build --platform --release --no-js --dts native.d.ts",
|
|
39
|
+
"build:debug": "napi build --platform --no-js --dts native.d.ts",
|
|
40
|
+
"bench": "node --expose-gc bench/lookup.js",
|
|
41
|
+
"release": "dev-bin/release.sh",
|
|
42
|
+
"test": "node --test test/*.test.js",
|
|
43
|
+
"test:pack": "node test/pack-smoke.js",
|
|
44
|
+
"typecheck": "tsc --noEmit"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"maxmind",
|
|
48
|
+
"mmdb",
|
|
49
|
+
"geoip",
|
|
50
|
+
"geoip2"
|
|
51
|
+
],
|
|
52
|
+
"license": "ISC",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@napi-rs/cli": "^3.7.2",
|
|
55
|
+
"@types/node": "^25.9.3",
|
|
56
|
+
"typescript": "^5.9.2"
|
|
57
|
+
},
|
|
58
|
+
"engines": {
|
|
59
|
+
"node": ">=20"
|
|
60
|
+
}
|
|
61
|
+
}
|