hostinfo 1.0.0 → 2.0.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  [![npm version](https://img.shields.io/npm/v/hostinfo)](https://www.npmjs.com/package/hostinfo)
5
5
  [![npm downloads](https://img.shields.io/npm/dm/hostinfo)](https://www.npmjs.com/package/hostinfo)
6
6
 
7
- Geocode IP addresses to city, country, and coordinates using the free, community-built [hostip.info](https://www.hostip.info/) API. Zero dependencies.
7
+ Geocode IP addresses to city, country, and coordinates using the free, community-built [hostip.info](https://www.hostip.info/) API. When hostip.info has no answer, it can fall back to reverse DNS and traceroute. Zero dependencies.
8
8
 
9
9
  ```js
10
10
  import { lookup } from 'hostinfo';
@@ -26,30 +26,171 @@ const info = await lookup('8.8.8.8');
26
26
  npm install hostinfo
27
27
  ```
28
28
 
29
- Requires Node.js 18 or newer. Works from both ESM (`import`) and CommonJS (`require`), and ships TypeScript types.
29
+ Requires Node.js 22.12 or newer. Written in TypeScript and published as ESM with bundled types; CommonJS callers can still `require('hostinfo')` thanks to Node's built-in `require(esm)` support.
30
30
 
31
31
  ## API
32
32
 
33
33
  ### `lookup(ip?, options?) → Promise<HostInfo>`
34
34
 
35
- Looks up an IPv4 address. Omit `ip` to geocode the caller's own public address:
35
+ Looks up an IPv4 address (anything else rejects with a `TypeError`, since hostip.info has no IPv6 data). Omit `ip` to geocode the caller's own public address:
36
36
 
37
37
  ```js
38
38
  const whereAmI = await lookup();
39
39
  ```
40
40
 
41
- Fields that hostip.info does not know are `null` — for private addresses and unrecognized IPs you'll get `city: null`, `country: null`, and `countryCode: 'XX'`. Coordinates are only present for IPs mapped to a city.
41
+ Fields that hostip.info does not know are `null`, including `countryCode` (the API's `"XX"` placeholder becomes `null`). Coordinates are only present for IPs mapped to a city. Private addresses (see [`isPrivateIPv4`](#isprivateipv4ip--boolean)) resolve to all-`null` fields immediately, without a request.
42
42
 
43
43
  **Options**
44
44
 
45
45
  | option | default | |
46
46
  | --- | --- | --- |
47
47
  | `timeout` | `10000` | Milliseconds before the request aborts. `0` disables. |
48
- | `signal` | – | Your own `AbortSignal`; overrides `timeout`. |
48
+ | `signal` | – | Your own `AbortSignal`; combined with `timeout`, whichever fires first aborts. |
49
49
  | `endpoint` | `https://api.hostip.info/` | Alternate API base URL. |
50
50
 
51
51
  Failures (network, HTTP status, unparseable response) reject with a `HostInfoError`; network errors keep the underlying error on `.cause`.
52
52
 
53
+ ### `locate(ip, options?) → Promise<Location>`
54
+
55
+ Best-effort geolocation when hostip.info comes up empty. It tries, in order:
56
+
57
+ 1. **hostip.info** — same as `lookup()`.
58
+ 2. **Reverse DNS** — network operators usually put a location code in router and server hostnames (`108-254-2-1.lightspeed.hstntx.sbcglobal.net` → Houston, `ae-5.r21.lsanca07.us.bb.gin.ntt.net` → Los Angeles). `locate` recognizes CLLI codes, airport codes, and city names for about 120 major network hubs.
59
+ 3. **Traceroute** (opt-in with `trace: true`) — runs a traceroute to the target and borrows the location of the closest hop that has one.
60
+
61
+ ```js
62
+ import { locate } from 'hostinfo';
63
+
64
+ const where = await locate('32.130.20.13', { trace: true });
65
+ // {
66
+ // ip: '32.130.20.13', city: 'Richardson, TX', country: 'UNITED STATES', countryCode: 'US',
67
+ // latitude: null, longitude: null, hostname: null,
68
+ // source: 'traceroute',
69
+ // via: { hop: 4, ip: '71.149.39.230', rtt: 13.2, hostname: null, info: { … }, hint: null }
70
+ // }
71
+ ```
72
+
73
+ The result has all the `HostInfo` fields plus:
74
+
75
+ | field | |
76
+ | --- | --- |
77
+ | `hostname` | The target's reverse DNS name, or `null`. |
78
+ | `source` | Where `city`/coordinates came from: `'hostip'`, `'hostname'`, `'traceroute'`, or `null` if nothing was found. |
79
+ | `via` | For `'traceroute'`, the hop whose location was used. |
80
+
81
+ Takes the `lookup()` options plus `trace` (`true` or [trace options](#traceip-options--promisetraceresult)). Treat `'hostname'` and especially `'traceroute'` answers as rough: the hop nearest the target is often in the same metro area, but it can also be a regional hub hundreds of miles away.
82
+
83
+ ### `trace(ip, options?) → Promise<TraceResult>`
84
+
85
+ Runs the system `traceroute` (macOS/Linux) or `tracert` (Windows) to an IPv4 address. It reports every hop, and where the trail goes cold if the target never answers:
86
+
87
+ ```js
88
+ import { trace } from 'hostinfo';
89
+
90
+ const { hops, reached, stopped, lastResponding } = await trace('198.51.100.7');
91
+ if (!reached) console.log(`Dropped after hop ${lastResponding?.hop} (${lastResponding?.hostname ?? lastResponding?.ip})`);
92
+ ```
93
+
94
+ Each hop is `{ hop, ip, rtt, hostname, info, hint }`. `ip` is `null` for a probe that timed out (`*`). `info` is hostip.info's answer, and `hint` is the location guessed from `hostname`. [Private hops](#isprivateipv4ip--boolean) are never sent to hostip.info. `stopped` is `'reached'`, `'max-hops'`, `'gave-up'`, or `'timeout'`.
95
+
96
+ | option | default | |
97
+ | --- | --- | --- |
98
+ | `maxHops` | `30` | Highest TTL to probe. |
99
+ | `probeTimeout` | `1000` | Milliseconds to wait per probe. macOS/Linux round this up to whole seconds. |
100
+ | `giveUpAfter` | `5` | Stop after this many silent hops in a row. `0` runs all the way to `maxHops`. |
101
+ | `timeout` | `60000` | Cut the trace short after this many milliseconds and return the hops so far. `0` disables. |
102
+ | `signal` | – | Abort the trace. The promise rejects with the signal's reason. |
103
+ | `geolocate` | `true` | Look up each hop's hostname and location. |
104
+ | `endpoint` | – | Alternate hostip.info endpoint. |
105
+
106
+ Rejects with a `HostInfoError` if traceroute isn't installed (e.g. minimal Linux containers: `apt install traceroute`).
107
+
108
+ ### `reverse(ip, options?) → Promise<string[]>`
109
+
110
+ Reverse DNS (PTR) lookup for an IPv4 or IPv6 address. Returns `[]` when there's no record. Options: `timeout` (default `5000`) and `signal`.
111
+
112
+ ### `whois(ip, options?) → Promise<WhoisInfo>`
113
+
114
+ Registration data for the network an IPv4 or IPv6 address belongs to, from [ARIN's RDAP service](https://www.arin.net/resources/registry/whois/rdap/). ARIN redirects addresses held by other registries (RIPE, APNIC, LACNIC, AFRINIC) to the right one automatically.
115
+
116
+ ```js
117
+ const info = await whois('8.8.8.8');
118
+ // {
119
+ // organization: 'Google LLC', name: 'GOGL', handle: 'NET-8-8-8-0-2',
120
+ // startAddress: '8.8.8.0', endAddress: '8.8.8.255', cidrs: ['8.8.8.0/24'],
121
+ // type: 'DIRECT ALLOCATION', abuseEmail: 'network-abuse@google.com',
122
+ // registry: 'whois.arin.net', contacts: [ … ], …
123
+ // }
124
+ ```
125
+
126
+ `contacts` lists every entity in the record with its `roles`, `name`, `org`, `email`, `phone`, and `address`. Options: `timeout` (default `10000`), `signal`, and `endpoint` (an alternate RDAP base URL). Rejects with a `HostInfoError` on network or HTTP errors.
127
+
128
+ ### `hintFromHostname(hostname) → LocationHint | null`
129
+
130
+ The hostname heuristic on its own: `{ code, city, countryCode, latitude, longitude }` or `null`.
131
+
132
+ ```js
133
+ hintFromHostname('be2345.ccr41.fra03.atlas.cogentco.com'); // { code: 'fra', city: 'Frankfurt', … }
134
+ ```
135
+
136
+ ### `isPrivateIPv4(ip) → boolean`
137
+
138
+ True for IPv4 addresses that aren't publicly routable: loopback, link-local, RFC 1918, carrier-grade NAT (`100.64.0.0/10`), IETF protocol assignments (`192.0.0.0/24`), benchmarking (`198.18.0.0/15`), multicast (`224.0.0.0/4`), and reserved space (`240.0.0.0/4`). The TEST-NET documentation ranges are deliberately not included. `false` for anything that isn't an IPv4 address.
139
+
140
+ ### Command line
141
+
142
+ ```sh
143
+ npx hostinfo 8.8.8.8 # locate an address (or omit it for your own public IP)
144
+ npx hostinfo 8.8.8.8 --trace # fall back to a traceroute if needed
145
+ npx hostinfo trace example.com
146
+ npx hostinfo reverse 1.1.1.1
147
+ npx hostinfo whois 8.8.8.8 # owner, range, and abuse contact from ARIN
148
+ npx hostinfo mcp # run as an MCP server (see below)
149
+ ```
150
+
151
+ Hostnames are resolved to an IPv4 address first. Add `--json` for machine-readable output.
152
+
153
+ ```
154
+ $ npx hostinfo trace 1.1.1.1
155
+ traceroute to 1.1.1.1
156
+ 1 192.168.86.1 9.518 ms
157
+ 2 192.168.1.254 12.512 ms
158
+ 3 108.254.2.1 13.706 ms 108-254-2-1.lightspeed.hstntx.sbcglobal.net Houston, TX? (from "hstntx")
159
+ 4 71.149.39.230 12.02 ms Richardson, TX, UNITED STATES
160
+ ...
161
+ 11 1.1.1.1 20.103 ms one.one.one.one Buffalo, NY, UNITED STATES
162
+ ```
163
+
164
+ A `?` marks a location guessed from the hostname rather than reported by hostip.info.
165
+
166
+ ### MCP server (for AI assistants)
167
+
168
+ `hostinfo mcp` runs a [Model Context Protocol](https://modelcontextprotocol.io/) server over stdio, so Claude, Cursor, VS Code, and other MCP clients can geolocate, trace, and WHOIS addresses for you. Add it to your client's config:
169
+
170
+ ```json
171
+ {
172
+ "mcpServers": {
173
+ "hostinfo": { "command": "npx", "args": ["-y", "hostinfo", "mcp"] }
174
+ }
175
+ }
176
+ ```
177
+
178
+ With Claude Code: `claude mcp add hostinfo -- npx -y hostinfo mcp`.
179
+
180
+ | tool | arguments | |
181
+ | --- | --- | --- |
182
+ | `locate` | `target?`, `trace?` | [`locate()`](#locateip-options--promiselocation); omit `target` for the server's own public IP |
183
+ | `traceroute` | `target`, `maxHops?` | [`trace()`](#traceip-options--promisetraceresult) |
184
+ | `reverse_dns` | `target` | [`reverse()`](#reverseip-options--promisestring) |
185
+ | `whois` | `target` | [`whois()`](#whoisip-options--promisewhoisinfo) |
186
+ | `hostname_hint` | `hostname` | [`hintFromHostname()`](#hintfromhostnamehostname--locationhint--null) |
187
+
188
+ `target` can be an IP address or a hostname. All tools are read-only, and results are the same JSON the library returns. Like the rest of the package, the server has no dependencies. (Traceroute runs on the machine hosting the server, so it measures that machine's path.)
189
+
190
+ ### For AI coding tools
191
+
192
+ [`llms.txt`](llms.txt) is a condensed API reference for LLMs, and it ships in the package at `node_modules/hostinfo/llms.txt`. Contributors' agents should read [`AGENTS.md`](AGENTS.md).
193
+
53
194
  ### Callback style
54
195
 
55
196
  The original 2011 signature still works if you pass a function last:
@@ -63,6 +204,8 @@ lookup('8.8.8.8', (err, info) => {
63
204
  });
64
205
  ```
65
206
 
207
+ > **Upgrading from 1.x:** the package is now ESM-only (CommonJS `require` still works on Node 22.12+), Node 18/20 are no longer supported, non-IPv4 input rejects with a `TypeError` instead of silently returning "(Private Address)", an unknown `countryCode` is `null` instead of `'XX'`, private addresses no longer hit the network, and `signal` no longer disables `timeout`.
208
+ >
66
209
  > **Upgrading from 0.0.2:** the result is now the flat object shown above rather than the raw `xml2js` parse of the API response, and the `request`/`xml2js` dependencies are gone.
67
210
 
68
211
  ## Accuracy
@@ -72,19 +215,24 @@ hostip.info is a community-maintained database. It's free and requires no API ke
72
215
  ## Development
73
216
 
74
217
  ```sh
75
- npm test # unit tests (mocked fetch)
218
+ npm install
219
+ npm run typecheck # tsc over src and tests
220
+ npm test # builds dist/, then runs unit tests (mocked fetch) and package-export checks
76
221
  npm run test:live # also hits the real API
77
222
  ```
78
223
 
224
+ The published package runs on Node 22.12+, but the tests run the TypeScript sources directly, which needs type stripping (on by default from Node 22.18).
225
+
79
226
  ## Releasing
80
227
 
81
228
  Publishing is automated with GitHub Actions via [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) — no npm tokens stored in the repo:
82
229
 
83
230
  1. Bump `version` in `package.json`, commit, and push (CI must be green).
84
231
  2. Create a GitHub release with a matching `vX.Y.Z` tag.
85
- 3. The [publish workflow](.github/workflows/publish.yml) runs the tests and publishes to npm with provenance.
232
+ 3. The [publish workflow](.github/workflows/publish.yml) runs the tests and stages the version on npm (`npm stage publish`) with provenance.
233
+ 4. Approve the staged version with 2FA — on npmjs.com under **Staged Packages**, or with `npm stage approve <stage-id>` (the id is in the workflow log).
86
234
 
87
- One-time setup: on npmjs.com → package **Settings** → **Trusted Publisher**, select GitHub Actions with repository `neopunisher/node-hostip` and workflow `publish.yml`.
235
+ One-time setup: on npmjs.com → package **Settings** → **Trusted Publisher**, select GitHub Actions with repository `neopunisher/node-hostip` and workflow `publish.yml`, leaving direct `npm publish` disallowed (staging-only).
88
236
 
89
237
  ## License
90
238
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { locate } from './locate.js';
4
+ import { lookup } from './lookup.js';
5
+ import { serveMcp } from './mcp.js';
6
+ import { toIP, toIPv4 } from './resolve.js';
7
+ import { reverse } from './reverse.js';
8
+ import { trace } from './trace.js';
9
+ import { whois } from './whois.js';
10
+ const USAGE = `Usage:
11
+ hostinfo [ip|host] Locate an address (your own public IP if omitted)
12
+ hostinfo trace <ip|host> Traceroute, with each hop's hostname and location
13
+ hostinfo reverse <ip|host> Reverse DNS (PTR) names
14
+ hostinfo whois <ip|host> Network owner, range, and abuse contact (ARIN RDAP)
15
+ hostinfo mcp Run as an MCP server over stdio (for AI assistants)
16
+
17
+ Options:
18
+ --trace Let \`hostinfo <ip>\` fall back to a traceroute (slow)
19
+ --json Print raw JSON
20
+ -h, --help Show this help`;
21
+ function place(info) {
22
+ if (!info)
23
+ return null;
24
+ const parts = [info.city, info.country ?? info.countryCode].filter((p) => p !== null);
25
+ return parts.length > 0 ? parts.join(', ') : null;
26
+ }
27
+ function formatLocation(result) {
28
+ const lines = [result.ip ?? '(unknown ip)'];
29
+ if (result.hostname)
30
+ lines.push(` hostname ${result.hostname}`);
31
+ lines.push(` location ${place(result) ?? 'unknown'}`);
32
+ if (result.latitude !== null && result.longitude !== null) {
33
+ lines.push(` coords ${result.latitude}, ${result.longitude}`);
34
+ }
35
+ if (result.source) {
36
+ const via = result.via ? ` (hop ${result.via.hop}, ${result.via.ip})` : '';
37
+ lines.push(` source ${result.source}${via}`);
38
+ }
39
+ return lines.join('\n');
40
+ }
41
+ function formatWhois(info) {
42
+ const range = info.startAddress && info.endAddress ? `${info.startAddress} - ${info.endAddress}` : null;
43
+ const rows = [
44
+ ['org', info.organization],
45
+ ['network', [info.name, info.handle && `(${info.handle})`].filter(Boolean).join(' ') || null],
46
+ ['range', [range, info.cidrs.join(', ')].filter(Boolean).join(' ') || null],
47
+ ['type', info.type],
48
+ ['country', info.country],
49
+ ['abuse', info.abuseEmail],
50
+ ['registered', info.registered],
51
+ ['updated', info.updated],
52
+ ['registry', info.registry],
53
+ ];
54
+ const lines = [info.ip];
55
+ for (const [label, value] of rows)
56
+ if (value)
57
+ lines.push(` ${label.padEnd(10)} ${value}`);
58
+ return lines.join('\n');
59
+ }
60
+ function formatHop(hop) {
61
+ const n = String(hop.hop).padStart(2);
62
+ if (hop.ip === null)
63
+ return `${n} *`;
64
+ const rtt = hop.rtt === null ? '' : `${hop.rtt} ms`;
65
+ const where = place(hop.info) ?? (hop.hint ? `${hop.hint.city}? (from "${hop.hint.code}")` : '');
66
+ return [`${n} ${hop.ip.padEnd(15)} ${rtt.padStart(10)}`, hop.hostname, where]
67
+ .filter(Boolean)
68
+ .join(' ');
69
+ }
70
+ function formatTrace(result) {
71
+ const lines = [`traceroute to ${result.target}`, ...result.hops.map(formatHop)];
72
+ if (!result.reached) {
73
+ const last = result.lastResponding;
74
+ lines.push(`did not reach ${result.target} (${result.stopped})`);
75
+ if (last)
76
+ lines.push(`last responding hop: ${last.hop} ${last.ip}`);
77
+ }
78
+ return lines.join('\n');
79
+ }
80
+ async function main() {
81
+ const { values, positionals } = parseArgs({
82
+ allowPositionals: true,
83
+ options: {
84
+ json: { type: 'boolean' },
85
+ trace: { type: 'boolean' },
86
+ help: { type: 'boolean', short: 'h' },
87
+ },
88
+ });
89
+ if (values.help)
90
+ return console.log(USAGE);
91
+ const [first, second] = positionals;
92
+ if (first === 'mcp')
93
+ return serveMcp();
94
+ const command = first === 'trace' || first === 'reverse' || first === 'whois' ? first : 'locate';
95
+ const target = command === 'locate' ? first : second;
96
+ if (command !== 'locate' && target === undefined) {
97
+ console.error(USAGE);
98
+ process.exitCode = 2;
99
+ return;
100
+ }
101
+ const controller = new AbortController();
102
+ process.once('SIGINT', () => controller.abort());
103
+ const { signal } = controller;
104
+ const print = (value, format) => console.log(values.json ? JSON.stringify(value, null, 2) : format());
105
+ if (command === 'trace') {
106
+ const result = await trace(await toIPv4(target), { signal });
107
+ return print(result, () => formatTrace(result));
108
+ }
109
+ if (command === 'whois') {
110
+ const info = await whois(await toIP(target), { signal });
111
+ return print(info, () => formatWhois(info));
112
+ }
113
+ if (command === 'reverse') {
114
+ const names = await reverse(await toIP(target), { signal });
115
+ return print(names, () => (names.length > 0 ? names.join('\n') : '(no PTR records)'));
116
+ }
117
+ // Without a target, ask hostip.info who we are first.
118
+ const ip = target === undefined ? (await lookup({ signal })).ip : await toIPv4(target);
119
+ if (ip === null)
120
+ throw new Error('hostip.info did not report your public IP');
121
+ const result = await locate(ip, { signal, ...(values.trace && { trace: { signal } }) });
122
+ print(result, () => formatLocation(result));
123
+ }
124
+ main().catch((error) => {
125
+ console.error(`hostinfo: ${error instanceof Error ? error.message : String(error)}`);
126
+ process.exitCode = 1;
127
+ });
@@ -0,0 +1,15 @@
1
+ export interface LocationHint {
2
+ /** The part of the hostname that matched, e.g. "lax" or "hstntx". */
3
+ code: string;
4
+ city: string;
5
+ countryCode: string;
6
+ latitude: number;
7
+ longitude: number;
8
+ }
9
+ /**
10
+ * Guess where a router or server sits from the location code embedded in its
11
+ * hostname (e.g. "ae-1.r20.lax01.us.bb.gin.ntt.net" → Los Angeles). Returns
12
+ * null when nothing recognizable is found. This is a heuristic: treat it as a
13
+ * hint, not a fact.
14
+ */
15
+ export declare function hintFromHostname(hostname: string): LocationHint | null;
package/dist/hints.js ADDED
@@ -0,0 +1,176 @@
1
+ // Network operators name routers after where they sit, using CLLI codes
2
+ // (6 letters, North America), IATA airport codes, or plain city names:
3
+ // 108-254-2-1.lightspeed.hstntx.sbcglobal.net -> Houston
4
+ // ae-5.r21.lsanca07.us.bb.gin.ntt.net -> Los Angeles
5
+ // be2345.ccr41.fra03.atlas.cogentco.com -> Frankfurt
6
+ // Each row: city, country code, latitude, longitude, space-separated codes.
7
+ const PLACES = [
8
+ // North America
9
+ ['Ashburn, VA', 'US', 39.04, -77.49, 'asbnva ashburn'],
10
+ ['Washington, DC', 'US', 38.9, -77.04, 'washdc iad dca washington'],
11
+ ['Reston, VA', 'US', 38.96, -77.36, 'rstnva reston'],
12
+ ['Richmond, VA', 'US', 37.54, -77.44, 'rcmdva ric richmond'],
13
+ ['Baltimore, MD', 'US', 39.29, -76.61, 'bltmmd bwi baltimore'],
14
+ ['New York, NY', 'US', 40.71, -74.01, 'nycmny nyc jfk lga newyork'],
15
+ ['Newark, NJ', 'US', 40.74, -74.17, 'nwrknj ewr newark'],
16
+ ['Philadelphia, PA', 'US', 39.95, -75.17, 'phlapa phl philadelphia'],
17
+ ['Pittsburgh, PA', 'US', 40.44, -80.0, 'pitbpa pit pittsburgh'],
18
+ ['Boston, MA', 'US', 42.36, -71.06, 'bstnma bos boston'],
19
+ ['Atlanta, GA', 'US', 33.75, -84.39, 'atlnga atl atlanta'],
20
+ ['Miami, FL', 'US', 25.76, -80.19, 'miamfl mia miami'],
21
+ ['Orlando, FL', 'US', 28.54, -81.38, 'orldfl mco orlando'],
22
+ ['Tampa, FL', 'US', 27.95, -82.46, 'tampfl tpa tampa'],
23
+ ['Jacksonville, FL', 'US', 30.33, -81.66, 'jcvlfl jax jacksonville'],
24
+ ['Charlotte, NC', 'US', 35.23, -80.84, 'chrlnc clt charlotte'],
25
+ ['Raleigh, NC', 'US', 35.78, -78.64, 'rlghnc rdu raleigh'],
26
+ ['Nashville, TN', 'US', 36.16, -86.78, 'nsvltn bna nashville'],
27
+ ['Chicago, IL', 'US', 41.88, -87.63, 'chcgil ord mdw chicago'],
28
+ ['Detroit, MI', 'US', 42.33, -83.05, 'dtrtmi dtw detroit'],
29
+ ['Cleveland, OH', 'US', 41.5, -81.69, 'clevoh cle cleveland'],
30
+ ['Columbus, OH', 'US', 39.96, -83.0, 'clmboh cmh columbus'],
31
+ ['Cincinnati, OH', 'US', 39.1, -84.51, 'cncnoh cvg cincinnati'],
32
+ ['Indianapolis, IN', 'US', 39.77, -86.16, 'ipltin ind indianapolis'],
33
+ ['Milwaukee, WI', 'US', 43.04, -87.91, 'mlwkwi mke milwaukee'],
34
+ ['Minneapolis, MN', 'US', 44.98, -93.27, 'mplsmn msp minneapolis'],
35
+ ['St. Louis, MO', 'US', 38.63, -90.2, 'stlsmo stl stlouis'],
36
+ ['Kansas City, MO', 'US', 39.1, -94.58, 'kscymo mci kansascity'],
37
+ ['Omaha, NE', 'US', 41.26, -95.93, 'omahne oma omaha'],
38
+ ['Dallas, TX', 'US', 32.78, -96.8, 'dllstx dfw dal dallas'],
39
+ ['Houston, TX', 'US', 29.76, -95.37, 'hstntx iah hou houston'],
40
+ ['Austin, TX', 'US', 30.27, -97.74, 'austtx aus austin'],
41
+ ['San Antonio, TX', 'US', 29.42, -98.49, 'snantx sanantonio'],
42
+ ['Oklahoma City, OK', 'US', 35.47, -97.52, 'okcyok okc oklahomacity'],
43
+ ['New Orleans, LA', 'US', 29.95, -90.07, 'nworla msy neworleans'],
44
+ ['Denver, CO', 'US', 39.74, -104.99, 'dnvrco den denver'],
45
+ ['Salt Lake City, UT', 'US', 40.76, -111.89, 'slkcut slc saltlake saltlakecity'],
46
+ ['Phoenix, AZ', 'US', 33.45, -112.07, 'phnxaz phx phoenix'],
47
+ ['Las Vegas, NV', 'US', 36.17, -115.14, 'lsvgnv las lasvegas'],
48
+ ['Los Angeles, CA', 'US', 34.05, -118.24, 'lsanca lax losangeles'],
49
+ ['San Diego, CA', 'US', 32.72, -117.16, 'sndgca sandiego'],
50
+ ['San Jose, CA', 'US', 37.34, -121.89, 'snjsca sjc sanjose'],
51
+ ['Santa Clara, CA', 'US', 37.35, -121.96, 'sntcca santaclara'],
52
+ ['Palo Alto, CA', 'US', 37.44, -122.14, 'plalca paloalto'],
53
+ ['San Francisco, CA', 'US', 37.77, -122.42, 'snfcca sfo sanfrancisco'],
54
+ ['Sacramento, CA', 'US', 38.58, -121.49, 'scrmca smf sacramento'],
55
+ ['Portland, OR', 'US', 45.52, -122.68, 'ptldor pdx portland'],
56
+ ['Seattle, WA', 'US', 47.61, -122.33, 'sttlwa sea seattle'],
57
+ ['Honolulu, HI', 'US', 21.31, -157.86, 'hnllhi hnl honolulu'],
58
+ ['Anchorage, AK', 'US', 61.22, -149.9, 'anchak anc anchorage'],
59
+ ['Toronto', 'CA', 43.65, -79.38, 'yyz toronto'],
60
+ ['Montreal', 'CA', 45.5, -73.57, 'mtl yul montreal'],
61
+ ['Vancouver', 'CA', 49.28, -123.12, 'yvr vancouver'],
62
+ ['Mexico City', 'MX', 19.43, -99.13, 'mex mexico'],
63
+ // Europe
64
+ ['London', 'GB', 51.51, -0.13, 'lon lhr london'],
65
+ ['Manchester', 'GB', 53.48, -2.24, 'manchester'],
66
+ ['Dublin', 'IE', 53.35, -6.26, 'dub dublin'],
67
+ ['Amsterdam', 'NL', 52.37, 4.9, 'ams amsterdam'],
68
+ ['Brussels', 'BE', 50.85, 4.35, 'bru brussels'],
69
+ ['Paris', 'FR', 48.86, 2.35, 'par cdg paris'],
70
+ ['Marseille', 'FR', 43.3, 5.37, 'mrs marseille'],
71
+ ['Frankfurt', 'DE', 50.11, 8.68, 'fra frankfurt'],
72
+ ['Berlin', 'DE', 52.52, 13.4, 'ber berlin'],
73
+ ['Munich', 'DE', 48.14, 11.58, 'muc munich muenchen'],
74
+ ['Hamburg', 'DE', 53.55, 9.99, 'hamburg'],
75
+ ['Düsseldorf', 'DE', 51.23, 6.77, 'dus duesseldorf dusseldorf'],
76
+ ['Zurich', 'CH', 47.38, 8.54, 'zrh zurich'],
77
+ ['Geneva', 'CH', 46.2, 6.14, 'gva geneva'],
78
+ ['Vienna', 'AT', 48.21, 16.37, 'vie vienna'],
79
+ ['Milan', 'IT', 45.46, 9.19, 'mxp lin milan'],
80
+ ['Rome', 'IT', 41.9, 12.5, 'fco rome'],
81
+ ['Madrid', 'ES', 40.42, -3.7, 'mad madrid'],
82
+ ['Barcelona', 'ES', 41.39, 2.17, 'bcn barcelona'],
83
+ ['Lisbon', 'PT', 38.72, -9.14, 'lis lisbon'],
84
+ ['Copenhagen', 'DK', 55.68, 12.57, 'cph copenhagen'],
85
+ ['Stockholm', 'SE', 59.33, 18.07, 'arn sto stockholm'],
86
+ ['Oslo', 'NO', 59.91, 10.75, 'osl oslo'],
87
+ ['Helsinki', 'FI', 60.17, 24.94, 'hel helsinki'],
88
+ ['Warsaw', 'PL', 52.23, 21.01, 'waw warsaw'],
89
+ ['Prague', 'CZ', 50.08, 14.44, 'prg prague'],
90
+ ['Budapest', 'HU', 47.5, 19.04, 'bud budapest'],
91
+ ['Bucharest', 'RO', 44.43, 26.1, 'otp bucharest'],
92
+ ['Sofia', 'BG', 42.7, 23.32, 'sof sofia'],
93
+ ['Athens', 'GR', 37.98, 23.73, 'ath athens'],
94
+ ['Istanbul', 'TR', 41.01, 28.98, 'ist istanbul'],
95
+ ['Kyiv', 'UA', 50.45, 30.52, 'kbp kiev kyiv'],
96
+ // Middle East & Africa
97
+ ['Tel Aviv', 'IL', 32.09, 34.78, 'tlv telaviv'],
98
+ ['Dubai', 'AE', 25.2, 55.27, 'dxb dubai'],
99
+ ['Johannesburg', 'ZA', -26.2, 28.05, 'jnb johannesburg'],
100
+ ['Cape Town', 'ZA', -33.92, 18.42, 'cpt capetown'],
101
+ ['Lagos', 'NG', 6.52, 3.38, 'lagos'],
102
+ ['Nairobi', 'KE', -1.29, 36.82, 'nbo nairobi'],
103
+ // Asia-Pacific
104
+ ['Tokyo', 'JP', 35.68, 139.69, 'tyo nrt hnd tokyo'],
105
+ ['Osaka', 'JP', 34.69, 135.5, 'osa kix osaka'],
106
+ ['Seoul', 'KR', 37.57, 126.98, 'sel icn seoul'],
107
+ ['Hong Kong', 'HK', 22.32, 114.17, 'hkg hongkong'],
108
+ ['Taipei', 'TW', 25.03, 121.57, 'tpe taipei'],
109
+ ['Singapore', 'SG', 1.35, 103.82, 'sin singapore'],
110
+ ['Kuala Lumpur', 'MY', 3.14, 101.69, 'kul kualalumpur'],
111
+ ['Bangkok', 'TH', 13.76, 100.5, 'bkk bangkok'],
112
+ ['Jakarta', 'ID', -6.21, 106.85, 'cgk jakarta'],
113
+ ['Manila', 'PH', 14.6, 120.98, 'mnl manila'],
114
+ ['Mumbai', 'IN', 19.08, 72.88, 'bom mumbai'],
115
+ ['Delhi', 'IN', 28.61, 77.21, 'del delhi'],
116
+ ['Chennai', 'IN', 13.08, 80.27, 'maa chennai'],
117
+ ['Sydney', 'AU', -33.87, 151.21, 'syd sydney'],
118
+ ['Melbourne', 'AU', -37.81, 144.96, 'mel melbourne'],
119
+ ['Perth', 'AU', -31.95, 115.86, 'perth'],
120
+ ['Auckland', 'NZ', -36.85, 174.76, 'akl auckland'],
121
+ // South America
122
+ ['São Paulo', 'BR', -23.55, -46.63, 'sao gru saopaulo'],
123
+ ['Rio de Janeiro', 'BR', -22.91, -43.17, 'rio gig riodejaneiro'],
124
+ ['Buenos Aires', 'AR', -34.6, -58.38, 'eze bue buenosaires'],
125
+ ['Santiago', 'CL', -33.45, -70.67, 'scl santiago'],
126
+ ['Bogotá', 'CO', 4.71, -74.07, 'bog bogota'],
127
+ ['Lima', 'PE', -12.05, -77.04, 'lim lima'],
128
+ ];
129
+ // Lower number wins: a 6-letter CLLI code is the most specific, a 3-letter
130
+ // airport code the most likely to collide with an unrelated abbreviation.
131
+ const CLLI = 0;
132
+ const NAME = 1;
133
+ const AIRPORT = 2;
134
+ const CODES = new Map();
135
+ for (const place of PLACES) {
136
+ const codes = place[4].split(' ');
137
+ // CLLI codes are exactly six letters ending in a two-letter state; the
138
+ // first code on a North American row is always its CLLI code.
139
+ const hasClli = place[1] === 'US' && codes[0].length === 6;
140
+ codes.forEach((code, i) => {
141
+ const rank = hasClli && i === 0 ? CLLI : code.length === 3 ? AIRPORT : NAME;
142
+ CODES.set(code, { rank, place });
143
+ });
144
+ }
145
+ /**
146
+ * Guess where a router or server sits from the location code embedded in its
147
+ * hostname (e.g. "ae-1.r20.lax01.us.bb.gin.ntt.net" → Los Angeles). Returns
148
+ * null when nothing recognizable is found. This is a heuristic: treat it as a
149
+ * hint, not a fact.
150
+ */
151
+ export function hintFromHostname(hostname) {
152
+ const labels = hostname.toLowerCase().replace(/\.$/, '').split('.');
153
+ // The registrable domain ("ntt.net", "cogentco.com") names the operator,
154
+ // not the location, so leave it out.
155
+ const words = labels
156
+ .slice(0, -2)
157
+ .flatMap((label) => label.split(/[^a-z]+/))
158
+ .filter((word) => word.length > 0);
159
+ // Also try adjacent words joined, so "san-jose" and "los-angeles" match.
160
+ const candidates = [
161
+ ...words,
162
+ ...words.slice(1).map((word, i) => words[i] + word),
163
+ ...words.slice(2).map((word, i) => words[i] + words[i + 1] + word),
164
+ ];
165
+ let best;
166
+ for (const code of candidates) {
167
+ const entry = CODES.get(code);
168
+ // Strict < keeps the leftmost match within a rank.
169
+ if (entry && (best === undefined || entry.rank < best.rank))
170
+ best = { code, ...entry };
171
+ }
172
+ if (!best)
173
+ return null;
174
+ const [city, countryCode, latitude, longitude] = best.place;
175
+ return { code: best.code, city, countryCode, latitude, longitude };
176
+ }
@@ -0,0 +1,25 @@
1
+ import { hintFromHostname } from './hints.ts';
2
+ import { locate } from './locate.ts';
3
+ import { HostInfoError, lookup } from './lookup.ts';
4
+ import { reverse } from './reverse.ts';
5
+ import { isPrivateIPv4 } from './private.ts';
6
+ import { trace } from './trace.ts';
7
+ import { whois } from './whois.ts';
8
+ export { hintFromHostname, type LocationHint } from './hints.ts';
9
+ export { locate, type LocateOptions, type Location } from './locate.ts';
10
+ export { HostInfoError, lookup, type HostInfo, type LookupCallback, type LookupOptions, } from './lookup.ts';
11
+ export { reverse, type ReverseOptions } from './reverse.ts';
12
+ export { isPrivateIPv4 } from './private.ts';
13
+ export { trace, type TraceHop, type TraceOptions, type TraceResult } from './trace.ts';
14
+ export { whois, type WhoisContact, type WhoisInfo, type WhoisOptions } from './whois.ts';
15
+ declare const hostinfo: {
16
+ lookup: typeof lookup;
17
+ locate: typeof locate;
18
+ reverse: typeof reverse;
19
+ trace: typeof trace;
20
+ whois: typeof whois;
21
+ hintFromHostname: typeof hintFromHostname;
22
+ isPrivateIPv4: typeof isPrivateIPv4;
23
+ HostInfoError: typeof HostInfoError;
24
+ };
25
+ export default hostinfo;
package/dist/index.js ADDED
@@ -0,0 +1,25 @@
1
+ import { hintFromHostname } from './hints.js';
2
+ import { locate } from './locate.js';
3
+ import { HostInfoError, lookup } from './lookup.js';
4
+ import { reverse } from './reverse.js';
5
+ import { isPrivateIPv4 } from './private.js';
6
+ import { trace } from './trace.js';
7
+ import { whois } from './whois.js';
8
+ export { hintFromHostname } from './hints.js';
9
+ export { locate } from './locate.js';
10
+ export { HostInfoError, lookup, } from './lookup.js';
11
+ export { reverse } from './reverse.js';
12
+ export { isPrivateIPv4 } from './private.js';
13
+ export { trace } from './trace.js';
14
+ export { whois } from './whois.js';
15
+ const hostinfo = {
16
+ lookup,
17
+ locate,
18
+ reverse,
19
+ trace,
20
+ whois,
21
+ hintFromHostname,
22
+ isPrivateIPv4,
23
+ HostInfoError,
24
+ };
25
+ export default hostinfo;
@@ -0,0 +1,28 @@
1
+ import { type HostInfo, type LookupOptions } from './lookup.ts';
2
+ import { type TraceHop, type TraceOptions } from './trace.ts';
3
+ export interface LocateOptions extends LookupOptions {
4
+ /**
5
+ * When neither hostip.info nor the target's hostname gives a city, traceroute
6
+ * to the target and borrow the location of the closest hop that has one.
7
+ * Pass `true` or trace options. Slow (seconds) — default: false.
8
+ */
9
+ trace?: boolean | TraceOptions;
10
+ }
11
+ export interface Location extends HostInfo {
12
+ /** The target's reverse DNS name, if it has one. */
13
+ hostname: string | null;
14
+ /**
15
+ * Where `city` and the coordinates came from: hostip.info, a location code
16
+ * in the target's hostname, or a traceroute hop. null when no city was found.
17
+ */
18
+ source: 'hostip' | 'hostname' | 'traceroute' | null;
19
+ /** For `source: 'traceroute'`, the hop whose location was used. */
20
+ via: TraceHop | null;
21
+ }
22
+ /**
23
+ * Best-effort geolocation for an IPv4 address. Asks hostip.info first, then
24
+ * falls back to location codes in the target's reverse DNS name (e.g.
25
+ * "…lax01.example.net"), and optionally to a traceroute. Check `source` to see
26
+ * how much to trust the answer.
27
+ */
28
+ export declare function locate(ip: string, options?: LocateOptions): Promise<Location>;