From 15de259b88d42c91d3260fa966b3ce95b523c993 Mon Sep 17 00:00:00 2001 From: Tom Mount Date: Tue, 9 Dec 2025 11:42:19 -0500 Subject: [PATCH 1/4] add resproxy changes --- CHANGELOG.md | 4 ++ package.json | 2 +- src/common.ts | 8 +++ src/index.ts | 3 + src/ipinfoResProxyWrapper.ts | 132 +++++++++++++++++++++++++++++++++++ src/version.ts | 2 +- 6 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 src/ipinfoResProxyWrapper.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b7c72d1..b6054d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 4.3.0 + +- Add support for IPinfo Residential Proxy API + ## 4.2.0 - Add support for IPinfo Core API diff --git a/package.json b/package.json index 6dea183..e47fe62 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-ipinfo", - "version": "4.2.0", + "version": "4.3.0", "description": "Official Node client library for IPinfo", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/src/common.ts b/src/common.ts index bade933..164d09c 100644 --- a/src/common.ts +++ b/src/common.ts @@ -2,6 +2,7 @@ export const HOST: string = "ipinfo.io"; export const HOST_LITE: string = "api.ipinfo.io/lite"; export const HOST_CORE: string = "api.ipinfo.io/lookup"; export const HOST_PLUS: string = "api.ipinfo.io/lookup"; +export const HOST_RES_PROXY: string = "ipinfo.io/resproxy"; // cache version export const CACHE_VSN: string = "1"; @@ -202,6 +203,13 @@ export interface IPinfoPlus { is_satellite: boolean; } +export interface IPinfoResProxy { + ip: string; + service: string; + last_seen: string; + percent_days_seen: number; +} + export interface Prefix { netblock: string; id: string; diff --git a/src/index.ts b/src/index.ts index dd9a167..ee57f67 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,6 +2,7 @@ import IPinfoWrapper from "./ipinfoWrapper"; import IPinfoLiteWrapper from "./ipinfoLiteWrapper"; import IPinfoCoreWrapper from "./ipinfoCoreWrapper"; import IPinfoPlusWrapper from "./ipinfoPlusWrapper"; +import IPinfoResProxyWrapper from "./ipinfoResProxyWrapper"; import Cache from "./cache/cache"; import LruCache from "./cache/lruCache"; import ApiLimitError from "./errors/apiLimitError"; @@ -15,6 +16,7 @@ export { IPinfoLiteWrapper, IPinfoCoreWrapper, IPinfoPlusWrapper, + IPinfoResProxyWrapper, ApiLimitError }; export { @@ -27,6 +29,7 @@ export { IPinfo, IPinfoCore, IPinfoPlus, + IPinfoResProxy, Prefix, Prefixes6, AsnResponse, diff --git a/src/ipinfoResProxyWrapper.ts b/src/ipinfoResProxyWrapper.ts new file mode 100644 index 0000000..1240d09 --- /dev/null +++ b/src/ipinfoResProxyWrapper.ts @@ -0,0 +1,132 @@ +import fetch from "node-fetch" +import type { RequestInit, Response } from "node-fetch" +import Cache from "./cache/cache" +import LruCache from "./cache/lruCache" +import ApiLimitError from "./errors/apiLimitError" +import { isInSubnet } from "subnet-check" +import { + REQUEST_TIMEOUT_DEFAULT, + CACHE_VSN, + HOST_RES_PROXY, + BOGON_NETWORKS, + IPinfoResProxy, + IPBogon +} from "./common" +import VERSION from "./version" + +const clientUserAgent = `IPinfoClient/nodejs/${VERSION}` + +export default class IPinfoResProxyWrapper { + private token: string; + private baseUrl: string; + private cache: Cache; + private timeout: number; + + /** + * Creates IPinfoResProxyWrapper object to communicate with the IPinfo Res Proxy API. + * + * @param token Token string provided by IPinfo for the registered user. + * @param cache An implementation of IPCache interface, or LruCache if not specified. + * @param timeout Request timeout in milliseconds, or 5000ms if not specified. 0 disables the timeout. + * @param baseUrl The base url to use for api requests, or "ipinfo.io" if not specified. + */ + constructor( + token: string, + cache?: Cache, + timeout?: number, + baseUrl?: string, + ) { + this.token = token + this.cache = cache || new LruCache() + this.timeout = + timeout === null || timeout === undefined + ? REQUEST_TIMEOUT_DEFAULT + : timeout + this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}` + } + + public static cacheKey(k: string): string { + return `${k}:${CACHE_VSN}` + } + + public async fetchApi( + path: string, + init: RequestInit = {} + ): Promise { + const headers = { + Accept: "application/json", + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/json", + "User-Agent": clientUserAgent + } + + const request = Object.assign( + { + timeout: this.timeout, + method: "GET", + compress: false, + }, + init, + { headers: Object.assign(headers, init.headers) }, + ) + + const url = [this.baseUrl, path].join( + !this.baseUrl.endsWith("/") && !path.startsWith("/") + ? "/" + : "" + ) + + return fetch(url, request).then((response: Response) => { + if (response.status === 429) { + throw new ApiLimitError() + } + + if (response.status >= 400) { + throw new Error( + `Received an error from the IPinfo API ` + + `(using authorization ${headers["Authorization"]}) ` + + `${response.status} ${response.statusText} ${response.url}` + ) + } + + return response + }) + } + + public async lookupIp( + ip: string | undefined = undefined + ): Promise { + if (ip && this.isBogon(ip)) { + return { ip, bogon: true } + } + + if (!ip) { + ip = "me" + } + + const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip)) + + if (data) { + return data + } + + return this.fetchApi(ip) + .then(async (response) => { + const ipinfo = (await response.json()) as IPinfoResProxy + this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo) + + return ipinfo + }) + } + + private isBogon(ip: string): boolean { + if (ip != "") { + for (let network of BOGON_NETWORKS) { + if (isInSubnet(ip, network)) { + return true + } + } + } + return false + } +} diff --git a/src/version.ts b/src/version.ts index 5065de7..2d339d1 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,3 +1,3 @@ -const VERSION = "4.2.0"; +const VERSION = "4.3.0"; export default VERSION; From 298e31340cdf0855d4404714ad9d66224e846cbd Mon Sep 17 00:00:00 2001 From: Tom Mount Date: Tue, 9 Dec 2025 11:52:39 -0500 Subject: [PATCH 2/4] add res proxy wrapper tests --- __tests__/ipinfoResProxyWrapper.test.ts | 54 +++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 __tests__/ipinfoResProxyWrapper.test.ts diff --git a/__tests__/ipinfoResProxyWrapper.test.ts b/__tests__/ipinfoResProxyWrapper.test.ts new file mode 100644 index 0000000..cca0cb7 --- /dev/null +++ b/__tests__/ipinfoResProxyWrapper.test.ts @@ -0,0 +1,54 @@ +import * as dotenv from "dotenv" +import { IPBogon, IPinfoResProxy } from "../src/common" +import IPinfoResProxyWrapper from "../src/ipinfoResProxyWrapper" + +const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip + +beforeAll(() => { + dotenv.config() +}) + +describe("IPinfoResProxyWrapper", () => { + testIfTokenIsSet("lookupIp", async () => { + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!) + + for (let i = 0; i < 5; i++) { + const data = (await wrapper.lookupIp("139.5.0.122")) as IPinfoResProxy + + expect(data.ip).toEqual("139.5.0.122") + expect(data.service).toBeDefined() + expect(typeof data.service).toBe("string") + expect(data.last_seen).toBeDefined() + expect(typeof data.last_seen).toBe("string") + expect(data.percent_days_seen).toBeDefined() + expect(typeof data.percent_days_seen).toBe("number") + } + }) + + testIfTokenIsSet("isBogon", async () => { + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!) + + const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon + expect(data.ip).toEqual("198.51.100.1") + expect(data.bogon).toEqual(true) + }) + + test("Error is thrown for invalid token", async () => { + const wrapper = new IPinfoResProxyWrapper("invalid-token") + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow() + }) + + test("Error is thrown when response cannot be parsed as JSON", async () => { + const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#" + const wrapper = new IPinfoResProxyWrapper("token", undefined, undefined, baseUrlWithUnparseableResponse) + + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow() + + const result = await wrapper + .lookupIp("1.2.3.4") + .then((_) => "parseable") + .catch((_) => "unparseable") + + expect(result).toEqual("unparseable") + }) +}) \ No newline at end of file From 87102c79fe92d9d5f613d23305ccdce0f6c39a3c Mon Sep 17 00:00:00 2001 From: Tom Mount Date: Tue, 9 Dec 2025 11:57:15 -0500 Subject: [PATCH 3/4] run prettier --- __tests__/ipinfoResProxyWrapper.test.ts | 71 +++++++++++---------- src/ipinfoResProxyWrapper.ts | 84 ++++++++++++------------- 2 files changed, 79 insertions(+), 76 deletions(-) diff --git a/__tests__/ipinfoResProxyWrapper.test.ts b/__tests__/ipinfoResProxyWrapper.test.ts index cca0cb7..01c9182 100644 --- a/__tests__/ipinfoResProxyWrapper.test.ts +++ b/__tests__/ipinfoResProxyWrapper.test.ts @@ -1,54 +1,61 @@ -import * as dotenv from "dotenv" -import { IPBogon, IPinfoResProxy } from "../src/common" -import IPinfoResProxyWrapper from "../src/ipinfoResProxyWrapper" +import * as dotenv from "dotenv"; +import { IPBogon, IPinfoResProxy } from "../src/common"; +import IPinfoResProxyWrapper from "../src/ipinfoResProxyWrapper"; -const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip +const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip; beforeAll(() => { - dotenv.config() -}) + dotenv.config(); +}); describe("IPinfoResProxyWrapper", () => { testIfTokenIsSet("lookupIp", async () => { - const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!) + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); for (let i = 0; i < 5; i++) { - const data = (await wrapper.lookupIp("139.5.0.122")) as IPinfoResProxy - - expect(data.ip).toEqual("139.5.0.122") - expect(data.service).toBeDefined() - expect(typeof data.service).toBe("string") - expect(data.last_seen).toBeDefined() - expect(typeof data.last_seen).toBe("string") - expect(data.percent_days_seen).toBeDefined() - expect(typeof data.percent_days_seen).toBe("number") + const data = (await wrapper.lookupIp( + "139.5.0.122" + )) as IPinfoResProxy; + + expect(data.ip).toEqual("139.5.0.122"); + expect(data.service).toBeDefined(); + expect(typeof data.service).toBe("string"); + expect(data.last_seen).toBeDefined(); + expect(typeof data.last_seen).toBe("string"); + expect(data.percent_days_seen).toBeDefined(); + expect(typeof data.percent_days_seen).toBe("number"); } - }) + }); testIfTokenIsSet("isBogon", async () => { - const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!) + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); - const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon - expect(data.ip).toEqual("198.51.100.1") - expect(data.bogon).toEqual(true) - }) + const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon; + expect(data.ip).toEqual("198.51.100.1"); + expect(data.bogon).toEqual(true); + }); test("Error is thrown for invalid token", async () => { - const wrapper = new IPinfoResProxyWrapper("invalid-token") - await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow() - }) + const wrapper = new IPinfoResProxyWrapper("invalid-token"); + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); + }); test("Error is thrown when response cannot be parsed as JSON", async () => { - const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#" - const wrapper = new IPinfoResProxyWrapper("token", undefined, undefined, baseUrlWithUnparseableResponse) + const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#"; + const wrapper = new IPinfoResProxyWrapper( + "token", + undefined, + undefined, + baseUrlWithUnparseableResponse + ); - await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow() + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); const result = await wrapper .lookupIp("1.2.3.4") .then((_) => "parseable") - .catch((_) => "unparseable") + .catch((_) => "unparseable"); - expect(result).toEqual("unparseable") - }) -}) \ No newline at end of file + expect(result).toEqual("unparseable"); + }); +}); diff --git a/src/ipinfoResProxyWrapper.ts b/src/ipinfoResProxyWrapper.ts index 1240d09..7a4f8fc 100644 --- a/src/ipinfoResProxyWrapper.ts +++ b/src/ipinfoResProxyWrapper.ts @@ -1,9 +1,8 @@ -import fetch from "node-fetch" -import type { RequestInit, Response } from "node-fetch" -import Cache from "./cache/cache" -import LruCache from "./cache/lruCache" -import ApiLimitError from "./errors/apiLimitError" -import { isInSubnet } from "subnet-check" +import type { RequestInit, Response } from "node-fetch"; +import Cache from "./cache/cache"; +import LruCache from "./cache/lruCache"; +import ApiLimitError from "./errors/apiLimitError"; +import { isInSubnet } from "subnet-check"; import { REQUEST_TIMEOUT_DEFAULT, CACHE_VSN, @@ -11,10 +10,10 @@ import { BOGON_NETWORKS, IPinfoResProxy, IPBogon -} from "./common" -import VERSION from "./version" +} from "./common"; +import VERSION from "./version"; -const clientUserAgent = `IPinfoClient/nodejs/${VERSION}` +const clientUserAgent = `IPinfoClient/nodejs/${VERSION}`; export default class IPinfoResProxyWrapper { private token: string; @@ -24,7 +23,7 @@ export default class IPinfoResProxyWrapper { /** * Creates IPinfoResProxyWrapper object to communicate with the IPinfo Res Proxy API. - * + * * @param token Token string provided by IPinfo for the registered user. * @param cache An implementation of IPCache interface, or LruCache if not specified. * @param timeout Request timeout in milliseconds, or 5000ms if not specified. 0 disables the timeout. @@ -34,19 +33,19 @@ export default class IPinfoResProxyWrapper { token: string, cache?: Cache, timeout?: number, - baseUrl?: string, + baseUrl?: string ) { - this.token = token - this.cache = cache || new LruCache() + this.token = token; + this.cache = cache || new LruCache(); this.timeout = timeout === null || timeout === undefined ? REQUEST_TIMEOUT_DEFAULT - : timeout - this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}` + : timeout; + this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}`; } public static cacheKey(k: string): string { - return `${k}:${CACHE_VSN}` + return `${k}:${CACHE_VSN}`; } public async fetchApi( @@ -58,75 +57,72 @@ export default class IPinfoResProxyWrapper { Authorization: `Bearer ${this.token}`, "Content-Type": "application/json", "User-Agent": clientUserAgent - } + }; const request = Object.assign( { timeout: this.timeout, method: "GET", - compress: false, + compress: false }, init, - { headers: Object.assign(headers, init.headers) }, - ) + { headers: Object.assign(headers, init.headers) } + ); const url = [this.baseUrl, path].join( - !this.baseUrl.endsWith("/") && !path.startsWith("/") - ? "/" - : "" - ) + !this.baseUrl.endsWith("/") && !path.startsWith("/") ? "/" : "" + ); return fetch(url, request).then((response: Response) => { if (response.status === 429) { - throw new ApiLimitError() + throw new ApiLimitError(); } if (response.status >= 400) { - throw new Error( + throw new Error( `Received an error from the IPinfo API ` + - `(using authorization ${headers["Authorization"]}) ` + - `${response.status} ${response.statusText} ${response.url}` - ) + `(using authorization ${headers["Authorization"]}) ` + + `${response.status} ${response.statusText} ${response.url}` + ); } - return response - }) + return response; + }); } public async lookupIp( ip: string | undefined = undefined ): Promise { if (ip && this.isBogon(ip)) { - return { ip, bogon: true } + return { ip, bogon: true }; } if (!ip) { - ip = "me" + ip = "me"; } - const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip)) + const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip)); if (data) { - return data + return data; } - return this.fetchApi(ip) - .then(async (response) => { - const ipinfo = (await response.json()) as IPinfoResProxy - this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo) + return this.fetchApi(ip).then(async (response) => { + const ipinfo = (await response.json()) as IPinfoResProxy; + this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo); - return ipinfo - }) + return ipinfo; + }); } private isBogon(ip: string): boolean { if (ip != "") { for (let network of BOGON_NETWORKS) { if (isInSubnet(ip, network)) { - return true + return true; } } } - return false + return false; } -} +} \ No newline at end of file From ef72fd98dbf302629aec75660dfcbe604dc912c3 Mon Sep 17 00:00:00 2001 From: Tom Mount Date: Tue, 9 Dec 2025 12:02:18 -0500 Subject: [PATCH 4/4] additional prettier results --- __tests__/ipinfoResProxyWrapper.test.ts | 100 ++++++------ src/ipinfoResProxyWrapper.ts | 208 ++++++++++++------------ 2 files changed, 154 insertions(+), 154 deletions(-) diff --git a/__tests__/ipinfoResProxyWrapper.test.ts b/__tests__/ipinfoResProxyWrapper.test.ts index 01c9182..2580e5c 100644 --- a/__tests__/ipinfoResProxyWrapper.test.ts +++ b/__tests__/ipinfoResProxyWrapper.test.ts @@ -5,57 +5,57 @@ import IPinfoResProxyWrapper from "../src/ipinfoResProxyWrapper"; const testIfTokenIsSet = process.env.IPINFO_TOKEN ? test : test.skip; beforeAll(() => { - dotenv.config(); + dotenv.config(); }); describe("IPinfoResProxyWrapper", () => { - testIfTokenIsSet("lookupIp", async () => { - const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); - - for (let i = 0; i < 5; i++) { - const data = (await wrapper.lookupIp( - "139.5.0.122" - )) as IPinfoResProxy; - - expect(data.ip).toEqual("139.5.0.122"); - expect(data.service).toBeDefined(); - expect(typeof data.service).toBe("string"); - expect(data.last_seen).toBeDefined(); - expect(typeof data.last_seen).toBe("string"); - expect(data.percent_days_seen).toBeDefined(); - expect(typeof data.percent_days_seen).toBe("number"); - } - }); - - testIfTokenIsSet("isBogon", async () => { - const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); - - const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon; - expect(data.ip).toEqual("198.51.100.1"); - expect(data.bogon).toEqual(true); - }); - - test("Error is thrown for invalid token", async () => { - const wrapper = new IPinfoResProxyWrapper("invalid-token"); - await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); - }); - - test("Error is thrown when response cannot be parsed as JSON", async () => { - const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#"; - const wrapper = new IPinfoResProxyWrapper( - "token", - undefined, - undefined, - baseUrlWithUnparseableResponse - ); - - await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); - - const result = await wrapper - .lookupIp("1.2.3.4") - .then((_) => "parseable") - .catch((_) => "unparseable"); - - expect(result).toEqual("unparseable"); - }); + testIfTokenIsSet("lookupIp", async () => { + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); + + for (let i = 0; i < 5; i++) { + const data = (await wrapper.lookupIp( + "139.5.0.122" + )) as IPinfoResProxy; + + expect(data.ip).toEqual("139.5.0.122"); + expect(data.service).toBeDefined(); + expect(typeof data.service).toBe("string"); + expect(data.last_seen).toBeDefined(); + expect(typeof data.last_seen).toBe("string"); + expect(data.percent_days_seen).toBeDefined(); + expect(typeof data.percent_days_seen).toBe("number"); + } + }); + + testIfTokenIsSet("isBogon", async () => { + const wrapper = new IPinfoResProxyWrapper(process.env.IPINFO_TOKEN!); + + const data = (await wrapper.lookupIp("198.51.100.1")) as IPBogon; + expect(data.ip).toEqual("198.51.100.1"); + expect(data.bogon).toEqual(true); + }); + + test("Error is thrown for invalid token", async () => { + const wrapper = new IPinfoResProxyWrapper("invalid-token"); + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); + }); + + test("Error is thrown when response cannot be parsed as JSON", async () => { + const baseUrlWithUnparseableResponse = "https://ipinfo.io/developers#"; + const wrapper = new IPinfoResProxyWrapper( + "token", + undefined, + undefined, + baseUrlWithUnparseableResponse + ); + + await expect(wrapper.lookupIp("1.2.3.4")).rejects.toThrow(); + + const result = await wrapper + .lookupIp("1.2.3.4") + .then((_) => "parseable") + .catch((_) => "unparseable"); + + expect(result).toEqual("unparseable"); + }); }); diff --git a/src/ipinfoResProxyWrapper.ts b/src/ipinfoResProxyWrapper.ts index 7a4f8fc..892e2b7 100644 --- a/src/ipinfoResProxyWrapper.ts +++ b/src/ipinfoResProxyWrapper.ts @@ -4,125 +4,125 @@ import LruCache from "./cache/lruCache"; import ApiLimitError from "./errors/apiLimitError"; import { isInSubnet } from "subnet-check"; import { - REQUEST_TIMEOUT_DEFAULT, - CACHE_VSN, - HOST_RES_PROXY, - BOGON_NETWORKS, - IPinfoResProxy, - IPBogon + REQUEST_TIMEOUT_DEFAULT, + CACHE_VSN, + HOST_RES_PROXY, + BOGON_NETWORKS, + IPinfoResProxy, + IPBogon } from "./common"; import VERSION from "./version"; const clientUserAgent = `IPinfoClient/nodejs/${VERSION}`; export default class IPinfoResProxyWrapper { - private token: string; - private baseUrl: string; - private cache: Cache; - private timeout: number; - - /** - * Creates IPinfoResProxyWrapper object to communicate with the IPinfo Res Proxy API. - * - * @param token Token string provided by IPinfo for the registered user. - * @param cache An implementation of IPCache interface, or LruCache if not specified. - * @param timeout Request timeout in milliseconds, or 5000ms if not specified. 0 disables the timeout. - * @param baseUrl The base url to use for api requests, or "ipinfo.io" if not specified. - */ - constructor( - token: string, - cache?: Cache, - timeout?: number, - baseUrl?: string - ) { - this.token = token; - this.cache = cache || new LruCache(); - this.timeout = - timeout === null || timeout === undefined - ? REQUEST_TIMEOUT_DEFAULT - : timeout; - this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}`; - } - - public static cacheKey(k: string): string { - return `${k}:${CACHE_VSN}`; - } - - public async fetchApi( - path: string, - init: RequestInit = {} - ): Promise { - const headers = { - Accept: "application/json", - Authorization: `Bearer ${this.token}`, - "Content-Type": "application/json", - "User-Agent": clientUserAgent - }; - - const request = Object.assign( - { - timeout: this.timeout, - method: "GET", - compress: false - }, - init, - { headers: Object.assign(headers, init.headers) } - ); - - const url = [this.baseUrl, path].join( - !this.baseUrl.endsWith("/") && !path.startsWith("/") ? "/" : "" - ); - - return fetch(url, request).then((response: Response) => { - if (response.status === 429) { - throw new ApiLimitError(); - } - - if (response.status >= 400) { - throw new Error( - `Received an error from the IPinfo API ` + - `(using authorization ${headers["Authorization"]}) ` + - `${response.status} ${response.statusText} ${response.url}` + private token: string; + private baseUrl: string; + private cache: Cache; + private timeout: number; + + /** + * Creates IPinfoResProxyWrapper object to communicate with the IPinfo Res Proxy API. + * + * @param token Token string provided by IPinfo for the registered user. + * @param cache An implementation of IPCache interface, or LruCache if not specified. + * @param timeout Request timeout in milliseconds, or 5000ms if not specified. 0 disables the timeout. + * @param baseUrl The base url to use for api requests, or "ipinfo.io" if not specified. + */ + constructor( + token: string, + cache?: Cache, + timeout?: number, + baseUrl?: string + ) { + this.token = token; + this.cache = cache || new LruCache(); + this.timeout = + timeout === null || timeout === undefined + ? REQUEST_TIMEOUT_DEFAULT + : timeout; + this.baseUrl = baseUrl || `https://${HOST_RES_PROXY}`; + } + + public static cacheKey(k: string): string { + return `${k}:${CACHE_VSN}`; + } + + public async fetchApi( + path: string, + init: RequestInit = {} + ): Promise { + const headers = { + Accept: "application/json", + Authorization: `Bearer ${this.token}`, + "Content-Type": "application/json", + "User-Agent": clientUserAgent + }; + + const request = Object.assign( + { + timeout: this.timeout, + method: "GET", + compress: false + }, + init, + { headers: Object.assign(headers, init.headers) } ); - } - return response; - }); - } + const url = [this.baseUrl, path].join( + !this.baseUrl.endsWith("/") && !path.startsWith("/") ? "/" : "" + ); - public async lookupIp( - ip: string | undefined = undefined - ): Promise { - if (ip && this.isBogon(ip)) { - return { ip, bogon: true }; + return fetch(url, request).then((response: Response) => { + if (response.status === 429) { + throw new ApiLimitError(); + } + + if (response.status >= 400) { + throw new Error( + `Received an error from the IPinfo API ` + + `(using authorization ${headers["Authorization"]}) ` + + `${response.status} ${response.statusText} ${response.url}` + ); + } + + return response; + }); } - if (!ip) { - ip = "me"; - } + public async lookupIp( + ip: string | undefined = undefined + ): Promise { + if (ip && this.isBogon(ip)) { + return { ip, bogon: true }; + } - const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip)); + if (!ip) { + ip = "me"; + } - if (data) { - return data; - } + const data = await this.cache.get(IPinfoResProxyWrapper.cacheKey(ip)); - return this.fetchApi(ip).then(async (response) => { - const ipinfo = (await response.json()) as IPinfoResProxy; - this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo); + if (data) { + return data; + } + + return this.fetchApi(ip).then(async (response) => { + const ipinfo = (await response.json()) as IPinfoResProxy; + this.cache.set(IPinfoResProxyWrapper.cacheKey(ip), ipinfo); - return ipinfo; - }); - } + return ipinfo; + }); + } - private isBogon(ip: string): boolean { - if (ip != "") { - for (let network of BOGON_NETWORKS) { - if (isInSubnet(ip, network)) { - return true; + private isBogon(ip: string): boolean { + if (ip != "") { + for (let network of BOGON_NETWORKS) { + if (isInSubnet(ip, network)) { + return true; + } + } } - } + return false; } - return false; - } -} \ No newline at end of file +}