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 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
+ [![CI](https://github.com/neopunisher/node-hostip/actions/workflows/ci.yml/badge.svg)](https://github.com/neopunisher/node-hostip/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/hostinfo)](https://www.npmjs.com/package/hostinfo)
5
+ [![npm downloads](https://img.shields.io/npm/dm/hostinfo)](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
+ '&amp;': '&',
15
+ '&lt;': '<',
16
+ '&gt;': '>',
17
+ '&quot;': '"',
18
+ '&apos;': "'",
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
@@ -0,0 +1,4 @@
1
+ import hostinfo from './index.js';
2
+
3
+ export const { lookup, HostInfoError } = hostinfo;
4
+ export default hostinfo;
package/package.json CHANGED
@@ -1,32 +1,46 @@
1
1
  {
2
- "name": "hostinfo",
3
- "description": "Uses the hostinfo database to geocode ip addresses",
4
- "version": "0.0.1",
5
- "tags" : ["geoip", "hostinfo", "util", "utility"],
6
- "author" : "Carter Cole <node@cartercole.com>",
7
- "maintainers":[
8
- {
9
- "name":"Carter Cole",
10
- "email":"node@cartercole.com"
11
- }
12
- ],
13
- "homepage": "http://blog.cartercole.com",
14
- "repository" : {
15
- "type" : "git",
16
- "url" : "git://github.com/neopunisher/node-hostip.git"
17
- },
18
- "main" : "main.js",
19
- "licenses" : [
20
- {
21
- "type": "MIT",
22
- "url": "http://www.opensource.org/licenses/mit-license.php"
23
- }
24
- ],"bugs" :
25
- { "web" : "https://github.com/neopunisher/node-hostip/issues" },
26
- "dependencies": {
27
- "xml2js": ">= 0.1.9",
28
- "request": ">= 2.1.0"
29
- },
30
- "engine": [ "node >=0.4.1" ]
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
- }