hostinfo 0.0.1 → 1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/index.d.ts +47 -0
- package/index.js +118 -0
- package/index.mjs +4 -0
- package/package.json +44 -30
- package/README +0 -0
- package/main.js +0 -12
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2011-2026 Carter Cole
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# hostinfo
|
|
2
|
+
|
|
3
|
+
[](https://github.com/neopunisher/node-hostip/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/hostinfo)
|
|
5
|
+
[](https://www.npmjs.com/package/hostinfo)
|
|
6
|
+
|
|
7
|
+
Geocode IP addresses to city, country, and coordinates using the free, community-built [hostip.info](https://www.hostip.info/) API. Zero dependencies.
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { lookup } from 'hostinfo';
|
|
11
|
+
|
|
12
|
+
const info = await lookup('8.8.8.8');
|
|
13
|
+
// {
|
|
14
|
+
// ip: '8.8.8.8',
|
|
15
|
+
// city: 'Mountain View, CA',
|
|
16
|
+
// country: 'UNITED STATES',
|
|
17
|
+
// countryCode: 'US',
|
|
18
|
+
// latitude: 37.402,
|
|
19
|
+
// longitude: -122.078
|
|
20
|
+
// }
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm install hostinfo
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Requires Node.js 18 or newer. Works from both ESM (`import`) and CommonJS (`require`), and ships TypeScript types.
|
|
30
|
+
|
|
31
|
+
## API
|
|
32
|
+
|
|
33
|
+
### `lookup(ip?, options?) → Promise<HostInfo>`
|
|
34
|
+
|
|
35
|
+
Looks up an IPv4 address. Omit `ip` to geocode the caller's own public address:
|
|
36
|
+
|
|
37
|
+
```js
|
|
38
|
+
const whereAmI = await lookup();
|
|
39
|
+
```
|
|
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.
|
|
42
|
+
|
|
43
|
+
**Options**
|
|
44
|
+
|
|
45
|
+
| option | default | |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `timeout` | `10000` | Milliseconds before the request aborts. `0` disables. |
|
|
48
|
+
| `signal` | – | Your own `AbortSignal`; overrides `timeout`. |
|
|
49
|
+
| `endpoint` | `https://api.hostip.info/` | Alternate API base URL. |
|
|
50
|
+
|
|
51
|
+
Failures (network, HTTP status, unparseable response) reject with a `HostInfoError`; network errors keep the underlying error on `.cause`.
|
|
52
|
+
|
|
53
|
+
### Callback style
|
|
54
|
+
|
|
55
|
+
The original 2011 signature still works if you pass a function last:
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const { lookup } = require('hostinfo');
|
|
59
|
+
|
|
60
|
+
lookup('8.8.8.8', (err, info) => {
|
|
61
|
+
if (err) throw err;
|
|
62
|
+
console.log(info.city);
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **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
|
+
|
|
68
|
+
## Accuracy
|
|
69
|
+
|
|
70
|
+
hostip.info is a community-maintained database. It's free and requires no API key, but coverage and accuracy are modest compared to commercial GeoIP databases — treat results as approximate.
|
|
71
|
+
|
|
72
|
+
## Development
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
npm test # unit tests (mocked fetch)
|
|
76
|
+
npm run test:live # also hits the real API
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Releasing
|
|
80
|
+
|
|
81
|
+
Publishing is automated with GitHub Actions via [npm trusted publishing](https://docs.npmjs.com/trusted-publishers) — no npm tokens stored in the repo:
|
|
82
|
+
|
|
83
|
+
1. Bump `version` in `package.json`, commit, and push (CI must be green).
|
|
84
|
+
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.
|
|
86
|
+
|
|
87
|
+
One-time setup: on npmjs.com → package **Settings** → **Trusted Publisher**, select GitHub Actions with repository `neopunisher/node-hostip` and workflow `publish.yml`.
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT © Carter Cole
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface LookupOptions {
|
|
2
|
+
/**
|
|
3
|
+
* Milliseconds before the request is aborted. Ignored when `signal` is
|
|
4
|
+
* provided. Set to 0 to disable. Default: 10000.
|
|
5
|
+
*/
|
|
6
|
+
timeout?: number;
|
|
7
|
+
/** Abort the request yourself; overrides `timeout`. */
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
/** Alternate API endpoint. Default: "https://api.hostip.info/". */
|
|
10
|
+
endpoint?: string | URL;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface HostInfo {
|
|
14
|
+
/** The IP address the API answered for. */
|
|
15
|
+
ip: string | null;
|
|
16
|
+
/** City name, or null when unknown or a private address. */
|
|
17
|
+
city: string | null;
|
|
18
|
+
/** Country name (upper case), or null when unknown. */
|
|
19
|
+
country: string | null;
|
|
20
|
+
/** ISO-ish two-letter country code; "XX" when unknown. */
|
|
21
|
+
countryCode: string | null;
|
|
22
|
+
latitude: number | null;
|
|
23
|
+
longitude: number | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export type LookupCallback = (error: Error | null, result?: HostInfo) => void;
|
|
27
|
+
|
|
28
|
+
export class HostInfoError extends Error {
|
|
29
|
+
name: 'HostInfoError';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Geocode an IP address with the free hostip.info API. Omit `ip` to look up
|
|
34
|
+
* the caller's own public address. Returns a Promise unless a callback is
|
|
35
|
+
* given.
|
|
36
|
+
*/
|
|
37
|
+
export function lookup(ip?: string, options?: LookupOptions): Promise<HostInfo>;
|
|
38
|
+
export function lookup(options: LookupOptions): Promise<HostInfo>;
|
|
39
|
+
export function lookup(callback: LookupCallback): void;
|
|
40
|
+
export function lookup(ip: string | undefined, callback: LookupCallback): void;
|
|
41
|
+
export function lookup(ip: string | undefined, options: LookupOptions, callback: LookupCallback): void;
|
|
42
|
+
|
|
43
|
+
declare const hostinfo: {
|
|
44
|
+
lookup: typeof lookup;
|
|
45
|
+
HostInfoError: typeof HostInfoError;
|
|
46
|
+
};
|
|
47
|
+
export default hostinfo;
|
package/index.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ENDPOINT = 'https://api.hostip.info/';
|
|
4
|
+
const DEFAULT_TIMEOUT = 10_000;
|
|
5
|
+
|
|
6
|
+
class HostInfoError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = 'HostInfoError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const XML_ENTITIES = {
|
|
14
|
+
'&': '&',
|
|
15
|
+
'<': '<',
|
|
16
|
+
'>': '>',
|
|
17
|
+
'"': '"',
|
|
18
|
+
''': "'",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function unescapeXml(text) {
|
|
22
|
+
return text.replace(/&(?:amp|lt|gt|quot|apos);|&#(\d+);|&#x([0-9a-fA-F]+);/g, (match, dec, hex) => {
|
|
23
|
+
if (dec) return String.fromCodePoint(Number(dec));
|
|
24
|
+
if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
|
|
25
|
+
return XML_ENTITIES[match];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractField(xml, tag) {
|
|
30
|
+
const match = new RegExp(`<${tag}>([^<]*)</${tag}>`).exec(xml);
|
|
31
|
+
if (!match) return null;
|
|
32
|
+
const value = unescapeXml(match[1].trim());
|
|
33
|
+
return value === '' ? null : value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseHostipXml(xml) {
|
|
37
|
+
const block = /<Hostip>([\s\S]*?)<\/Hostip>/.exec(xml);
|
|
38
|
+
if (!block) {
|
|
39
|
+
throw new HostInfoError('Unexpected response from hostip.info: missing <Hostip> element');
|
|
40
|
+
}
|
|
41
|
+
const inner = block[1];
|
|
42
|
+
|
|
43
|
+
let city = extractField(inner, 'gml:name');
|
|
44
|
+
let country = extractField(inner, 'countryName');
|
|
45
|
+
// The API reports unknowns as placeholders like "(Unknown city)" or
|
|
46
|
+
// "(Private Address)"; normalize those to null.
|
|
47
|
+
if (city !== null && city.startsWith('(')) city = null;
|
|
48
|
+
if (country !== null && country.startsWith('(')) country = null;
|
|
49
|
+
|
|
50
|
+
let latitude = null;
|
|
51
|
+
let longitude = null;
|
|
52
|
+
const coordinates = extractField(inner, 'gml:coordinates');
|
|
53
|
+
if (coordinates !== null) {
|
|
54
|
+
// hostip.info returns "longitude,latitude"
|
|
55
|
+
const [lng, lat] = coordinates.split(',').map(Number);
|
|
56
|
+
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
|
57
|
+
latitude = lat;
|
|
58
|
+
longitude = lng;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
ip: extractField(inner, 'ip'),
|
|
64
|
+
city,
|
|
65
|
+
country,
|
|
66
|
+
countryCode: extractField(inner, 'countryAbbrev'),
|
|
67
|
+
latitude,
|
|
68
|
+
longitude,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function lookupAsync(ip, options) {
|
|
73
|
+
const { timeout = DEFAULT_TIMEOUT, signal, endpoint = DEFAULT_ENDPOINT } = options;
|
|
74
|
+
const url = new URL(endpoint);
|
|
75
|
+
if (ip !== undefined && ip !== null) url.searchParams.set('ip', String(ip));
|
|
76
|
+
|
|
77
|
+
let response;
|
|
78
|
+
try {
|
|
79
|
+
response = await fetch(url, {
|
|
80
|
+
headers: { accept: 'text/xml' },
|
|
81
|
+
signal: signal ?? (timeout > 0 ? AbortSignal.timeout(timeout) : undefined),
|
|
82
|
+
});
|
|
83
|
+
} catch (cause) {
|
|
84
|
+
throw new HostInfoError(`Request to hostip.info failed: ${cause.message}`, { cause });
|
|
85
|
+
}
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
throw new HostInfoError(`hostip.info responded with HTTP ${response.status}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The API serves ISO-8859-1, which response.text() would mangle.
|
|
91
|
+
const xml = new TextDecoder('iso-8859-1').decode(await response.arrayBuffer());
|
|
92
|
+
return parseHostipXml(xml);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function lookup(ip, options, callback) {
|
|
96
|
+
if (typeof ip === 'function') {
|
|
97
|
+
callback = ip;
|
|
98
|
+
ip = undefined;
|
|
99
|
+
options = undefined;
|
|
100
|
+
} else if (typeof options === 'function') {
|
|
101
|
+
callback = options;
|
|
102
|
+
options = undefined;
|
|
103
|
+
}
|
|
104
|
+
if (typeof ip === 'object' && ip !== null) {
|
|
105
|
+
options = ip;
|
|
106
|
+
ip = undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const promise = lookupAsync(ip, options ?? {});
|
|
110
|
+
if (typeof callback !== 'function') return promise;
|
|
111
|
+
promise.then(
|
|
112
|
+
(result) => callback(null, result),
|
|
113
|
+
(error) => callback(error),
|
|
114
|
+
);
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { lookup, HostInfoError };
|
package/index.mjs
ADDED
package/package.json
CHANGED
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
2
|
+
"name": "hostinfo",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Geocode IP addresses to city, country, and coordinates with the free hostip.info API. Zero dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"geoip",
|
|
7
|
+
"hostinfo",
|
|
8
|
+
"hostip",
|
|
9
|
+
"geocode",
|
|
10
|
+
"geolocation",
|
|
11
|
+
"ip"
|
|
12
|
+
],
|
|
13
|
+
"homepage": "https://github.com/neopunisher/node-hostip#readme",
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/neopunisher/node-hostip/issues"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/neopunisher/node-hostip.git"
|
|
20
|
+
},
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "Carter Cole <node@cartercole.com>",
|
|
23
|
+
"type": "commonjs",
|
|
24
|
+
"main": "index.js",
|
|
25
|
+
"types": "index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./index.d.ts",
|
|
29
|
+
"import": "./index.mjs",
|
|
30
|
+
"require": "./index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"index.js",
|
|
35
|
+
"index.mjs",
|
|
36
|
+
"index.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"test": "node --test",
|
|
44
|
+
"test:live": "LIVE_TEST=1 node --test"
|
|
45
|
+
}
|
|
31
46
|
}
|
|
32
|
-
|
package/README
DELETED
|
File without changes
|
package/main.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
var xml2js = require('xml2js'),
|
|
2
|
-
request = require('request');
|
|
3
|
-
|
|
4
|
-
exports.lookup = function (ip,cb){
|
|
5
|
-
request({uri: "http://api.hostip.info/?ip="+ip}, function (error, response, body) {
|
|
6
|
-
var parser = new xml2js.Parser();
|
|
7
|
-
parser.addListener('end', function(obj){
|
|
8
|
-
cb(null,obj);
|
|
9
|
-
});
|
|
10
|
-
parser.parseString(body);
|
|
11
|
-
});
|
|
12
|
-
}
|