UNPKG

@trap_stevo/geotide

Version:

Fuses IP intelligence, reverse geocoding, and radio/Wi-Fi triangulation into a single, real-time, precision-crafted API. Trace a single packet’s origin, map a million connections, or power real-time location-aware apps with elegance, accuracy, and streami

620 lines (619 loc) 19.2 kB
"use strict"; function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } const { EventEmitter } = require("events"); const net = require("net"); const { getClientIP } = require("./HUDManagers/IPUtilitiesManager.cjs"); function withTimeout(ms) { const controller = new AbortController(); const id = setTimeout(() => controller.abort(), ms); return { signal: controller.signal, cancel: () => clearTimeout(id) }; } ; function isPrivateIP(ip) { if (net.isIP(ip) === 4) { if (ip.startsWith("10.") || ip.startsWith("192.168.")) { return true; } if (ip.startsWith("172.")) { const n = parseInt(ip.split(".")[1], 10); if (n >= 16 && n <= 31) { return true; } } } if (net.isIP(ip) === 6) { const s = ip.toLowerCase(); if (s.startsWith("fc") || s.startsWith("fd") || s.startsWith("fe80")) { return true; } } return false; } ; function maskIP(ip) { if (net.isIP(ip) === 4) { return ip.replace(/\.\d+\.\d+$/, ".x.x"); } if (net.isIP(ip) === 6) { return ip.slice(0, 4) + "::xxxx"; } return ip; } ; function nn(v) { return v === undefined || v === null || v === "" ? null : v; } ; var _limit = /*#__PURE__*/new WeakMap(); var _map = /*#__PURE__*/new WeakMap(); class LRU { constructor(limit = 500) { _classPrivateFieldInitSpec(this, _limit, void 0); _classPrivateFieldInitSpec(this, _map, void 0); _classPrivateFieldSet(_limit, this, limit); _classPrivateFieldSet(_map, this, new Map()); } get(k) { if (!_classPrivateFieldGet(_map, this).has(k)) { return null; } const v = _classPrivateFieldGet(_map, this).get(k); _classPrivateFieldGet(_map, this).delete(k); _classPrivateFieldGet(_map, this).set(k, v); return v; } set(k, v) { if (_classPrivateFieldGet(_map, this).has(k)) { _classPrivateFieldGet(_map, this).delete(k); } _classPrivateFieldGet(_map, this).set(k, v); if (_classPrivateFieldGet(_map, this).size > _classPrivateFieldGet(_limit, this)) { _classPrivateFieldGet(_map, this).delete(_classPrivateFieldGet(_map, this).keys().next().value); } } } ; var _maxConcurrency = /*#__PURE__*/new WeakMap(); var _enableDebug = /*#__PURE__*/new WeakMap(); var _cacheTtlMs = /*#__PURE__*/new WeakMap(); var _deadlineMs = /*#__PURE__*/new WeakMap(); var _timeoutMs = /*#__PURE__*/new WeakMap(); var _providers = /*#__PURE__*/new WeakMap(); var _scoreOk = /*#__PURE__*/new WeakMap(); var _cache = /*#__PURE__*/new WeakMap(); var _nominatimUA = /*#__PURE__*/new WeakMap(); var _googleMapsKey = /*#__PURE__*/new WeakMap(); var _mapboxToken = /*#__PURE__*/new WeakMap(); var _revCache = /*#__PURE__*/new WeakMap(); var _GeoTide_brand = /*#__PURE__*/new WeakSet(); class GeoTide extends EventEmitter { constructor(_options = {}) { super(); _classPrivateMethodInitSpec(this, _GeoTide_brand); _classPrivateFieldInitSpec(this, _maxConcurrency, void 0); _classPrivateFieldInitSpec(this, _enableDebug, void 0); _classPrivateFieldInitSpec(this, _cacheTtlMs, void 0); _classPrivateFieldInitSpec(this, _deadlineMs, void 0); _classPrivateFieldInitSpec(this, _timeoutMs, void 0); _classPrivateFieldInitSpec(this, _providers, void 0); _classPrivateFieldInitSpec(this, _scoreOk, void 0); _classPrivateFieldInitSpec(this, _cache, void 0); _classPrivateFieldInitSpec(this, _nominatimUA, void 0); _classPrivateFieldInitSpec(this, _googleMapsKey, void 0); _classPrivateFieldInitSpec(this, _mapboxToken, void 0); _classPrivateFieldInitSpec(this, _revCache, void 0); _classPrivateFieldSet(_cacheTtlMs, this, _options.cacheTtlMs || 10 * 60 * 1000); _classPrivateFieldSet(_deadlineMs, this, _options.deadlineMs || 3000); _classPrivateFieldSet(_timeoutMs, this, _options.timeoutMs || 2500); _classPrivateFieldSet(_enableDebug, this, !!_options.enableDebug); _classPrivateFieldSet(_scoreOk, this, Number.isFinite(_options.scoreOk) ? _options.scoreOk : 5); _classPrivateFieldSet(_maxConcurrency, this, _options.maxConcurrency || 8); _classPrivateFieldSet(_revCache, this, new LRU(_options.reverseCacheSize || 1000)); _classPrivateFieldSet(_cache, this, new LRU(_options.cacheSize || 1000)); _classPrivateFieldSet(_providers, this, []); _classPrivateFieldSet(_nominatimUA, this, _options.nominatimUserAgent || "GeoTide/1.0 (geotide@sclpowerful.com)"); _classPrivateFieldSet(_googleMapsKey, this, _options.googleMapsKey || null); _classPrivateFieldSet(_mapboxToken, this, _options.mapboxToken || null); const ipgeoKey = _options.ipgeolocationKey || null; const ipinfoToken = _options.ipinfoToken || null; this.register("ipwhois", _assertClassBrand(_GeoTide_brand, this, _ipwhois).bind(this)); this.register("ipapi", _assertClassBrand(_GeoTide_brand, this, _ipapi).bind(this)); if (ipgeoKey) { this.register("ipgeolocation", (ip, o) => _assertClassBrand(_GeoTide_brand, this, _ipgeolocation).call(this, ipgeoKey, ip, o)); } if (ipinfoToken) { this.register("ipinfo", (ip, o) => _assertClassBrand(_GeoTide_brand, this, _ipinfo).call(this, ipinfoToken, ip, o)); } if (Array.isArray(_options.providers)) { for (const p of _options.providers) { if (p && typeof p.name === "string" && typeof p.fn === "function") { this.register(p.name, p.fn); } } } } register(name, fn) { _classPrivateFieldGet(_providers, this).push({ name, fn, healthyUntil: 0 }); } async lookup(ip) { if (!net.isIP(ip)) { throw new Error("Invalid IP"); } this.emit("lookup:start", { ip, ts: Date.now() }); if (isPrivateIP(ip)) { const v = { source: "local", ip, city: null, region: null, country: null, org: null, isp: null, loc: null, timezone: null, postal: null, flag: null, continent: null, confidence: 0 }; this.emit("lookup:result", { ip, result: v, cached: false, ts: Date.now() }); return v; } const ck = `geo:${ip}`; const cached = _classPrivateFieldGet(_cache, this).get(ck); if (cached && Date.now() - cached.t < _classPrivateFieldGet(_cacheTtlMs, this)) { this.emit("lookup:cache_hit", { ip, ts: Date.now() }); this.emit("lookup:result", { ip, result: cached.v, cached: true, ts: Date.now() }); return cached.v; } const start = Date.now(); const deadline = start + _classPrivateFieldGet(_deadlineMs, this); const best = await _assertClassBrand(_GeoTide_brand, this, _raceUntil).call(this, ip, deadline, _classPrivateFieldGet(_scoreOk, this)); if (!best) { return null; } const normalized = { source: best.source, ip, city: nn(best.city), region: nn(best.region), country: nn(best.country), org: nn(best.org), isp: nn(best.isp), loc: nn(best.loc), timezone: nn(best.timezone), postal: nn(best.postal), flag: nn(best.flag), continent: nn(best.continent), confidence: Math.min(1, best._score / 9) }; _classPrivateFieldGet(_cache, this).set(ck, { v: normalized, t: Date.now() }); this.emit("lookup:result", { ip, result: normalized, cached: false, ts: Date.now() }); return normalized; } async lookupMany(ips, options = {}) { const out = new Array(ips.length); const concurrency = options.concurrency || _classPrivateFieldGet(_maxConcurrency, this); let i = 0; const worker = async () => { while (i < ips.length) { const index = i++; const ip = ips[index]; try { out[index] = await this.lookup(ip); } catch (error) { out[index] = null; } } }; const n = Math.max(1, Math.min(concurrency, ips.length)); await Promise.all(Array.from({ length: n }, () => worker())); return out; } async reverse(lat, lon, options = {}) { const latNum = Number(lat), lonNum = Number(lon); if (!Number.isFinite(latNum) || !Number.isFinite(lonNum)) { console.log("[GeoTide] ~ Invalid lat/lon"); this.emit("reverse:error", { error: "invalid_lat_lon", lat, lon, ts: Date.now() }); return null; } this.emit("reverse:start", { lat: latNum, lon: lonNum, ts: Date.now() }); const ttl = options.cacheTtlMs ?? _classPrivateFieldGet(_cacheTtlMs, this); const ck = `rev:${latNum.toFixed(6)},${lonNum.toFixed(6)}`; const cached = _classPrivateFieldGet(_revCache, this).get(ck); if (cached && Date.now() - cached.t < ttl) { this.emit("reverse:cache_hit", { lat: latNum, lon: lonNum, ts: Date.now() }); this.emit("reverse:result", { lat: latNum, lon: lonNum, provider: cached.v.provider, result: cached.v, cached: true, ts: Date.now() }); return cached.v; } const start = Date.now(); const deadline = start + (options.deadlineMs ?? _classPrivateFieldGet(_deadlineMs, this)); const revFns = []; if (_classPrivateFieldGet(_googleMapsKey, this)) { revFns.push({ name: "google", fn: o => _assertClassBrand(_GeoTide_brand, this, _revGoogle).call(this, latNum, lonNum, o) }); } if (_classPrivateFieldGet(_mapboxToken, this)) { revFns.push({ name: "mapbox", fn: o => _assertClassBrand(_GeoTide_brand, this, _revMapbox).call(this, latNum, lonNum, o) }); } revFns.push({ name: "nominatim", fn: o => _assertClassBrand(_GeoTide_brand, this, _revNominatim).call(this, latNum, lonNum, o) }); const inflight = new Set(); const startCall = p => { const promise = _assertClassBrand(_GeoTide_brand, this, _callReverseBounded).call(this, p, deadline).catch(() => null).finally(() => inflight.delete(promise)); inflight.add(promise); return promise; }; const tasks = revFns.map(startCall); let best = null; while (inflight.size) { const val = await Promise.race(inflight); if (val && val.formatted) { best = val; break; } } if (!best) { const settled = await Promise.allSettled(tasks); for (const s of settled) { if (s.status === "fulfilled" && s.value?.formatted) { best = s.value; break; } } } if (!best) { return null; } const normalized = { provider: best.provider, formatted: best.formatted, components: best.components || {}, lat: latNum, lon: lonNum }; _classPrivateFieldGet(_revCache, this).set(ck, { v: normalized, t: Date.now() }); this.emit("reverse:result", { lat: latNum, lon: lonNum, provider: normalized.provider, result: normalized, cached: false, ts: Date.now() }); return normalized; } getClientIP(req) { return getClientIP(req); } } async function _raceUntil(ip, deadlineTs, scoreOk) { const healthy = _classPrivateFieldGet(_providers, this).filter(p => Date.now() >= p.healthyUntil); if (!healthy.length) { return null; } const inflight = new Set(); const results = []; const startCall = p => { const promise = _assertClassBrand(_GeoTide_brand, this, _callBounded).call(this, p, ip, deadlineTs).then(val => { if (val) { results.push(val); } return val; }).catch(() => null).finally(() => inflight.delete(promise)); inflight.add(promise); return promise; }; healthy.forEach(startCall); while (inflight.size) { const val = await Promise.race(inflight); if (val && val._score >= scoreOk) { return val; } } if (!results.length) { return null; } results.sort((a, b) => b._score - a._score || a._latency - b._latency); return results[0]; } async function _callBounded(p, ip, deadlineTs) { const remaining = Math.max(1, deadlineTs - Date.now()); const budget = Math.min(_classPrivateFieldGet(_timeoutMs, this), remaining); const t0 = Date.now(); const timer = withTimeout(budget); try { const v = await p.fn(ip, { signal: timer.signal }); timer.cancel(); if (!v) { throw new Error("no-data"); } v._latency = Date.now() - t0; v._score = _assertClassBrand(_GeoTide_brand, this, _score).call(this, v); if (_classPrivateFieldGet(_enableDebug, this)) { console.log(`[GeoTide] ${p.name} ${v._latency}ms score=${v._score} ip=${maskIP(ip)}`); } this.emit("provider:success", { ip, provider: p.name, latencyMs: v._latency, score: v._score, ts: Date.now() }); return v; } catch (error) { timer.cancel(); p.healthyUntil = Date.now() + 30_000; if (_classPrivateFieldGet(_enableDebug, this)) { console.warn(`[GeoTide] ${p.name} fail: ${error.message} ip=${maskIP(ip)}`); } this.emit("provider:error", { ip, provider: p.name, error: error.message, ts: Date.now() }); this.emit("provider:unhealthy", { provider: p.name, healthyUntil: p.healthyUntil, ts: Date.now() }); throw error; } } async function _callReverseBounded(p, deadlineTs) { const remaining = Math.max(1, deadlineTs - Date.now()); const budget = Math.min(_classPrivateFieldGet(_timeoutMs, this), remaining); const timer = withTimeout(budget); try { const val = await p.fn({ signal: timer.signal }); timer.cancel(); if (_classPrivateFieldGet(_enableDebug, this) && val) { console.log(`[GeoTide] ~ reverse:${p.name} ok`); } return val; } catch (error) { timer.cancel(); if (_classPrivateFieldGet(_enableDebug, this)) { console.warn(`[GeoTide] ~ reverse:${p.name} fail: ${error.message}`); } throw error; } } function _score(r) { let s = 0; ["city", "region", "country", "loc", "timezone", "postal", "org", "isp", "continent"].forEach(f => { if (r && nn(r[f])) { s++; } }); return s; } async function _ipwhois(ip, options = {}) { const res = await fetch(`https://ipwho.is/${ip}`, { signal: options.signal }); const data = await res.json(); if (!res.ok || !data?.success) { return null; } const lon = nn(data.longitude); const lat = nn(data.latitude); const loc = lat != null && lon != null ? `${lat},${lon}` : null; return { source: "ipwhois", timezone: nn(data.timezone?.id), continent: nn(data.continent), country: nn(data.country), region: nn(data.region), postal: nn(data.postal), city: nn(data.city), loc, org: nn(data.connection?.org), isp: nn(data.connection?.isp), flag: nn(data.flag?.emoji) }; } async function _ipapi(ip, options = {}) { const res = await fetch(`https://ipapi.co/${ip}/json/`, { signal: options.signal }); const data = await res.json(); if (!res.ok || data?.error) { return null; } const lon = nn(data.longitude); const lat = nn(data.latitude); const loc = lat != null && lon != null ? `${lat},${lon}` : null; return { source: "ipapi", timezone: nn(data.timezone), continent: nn(data.continent_code), country: nn(data.country_name), region: nn(data.region), postal: nn(data.postal), city: nn(data.city), loc, org: nn(data.org), isp: nn(data.org), flag: null }; } async function _ipinfo(token, ip, options = {}) { const res = await fetch(`https://ipinfo.io/${ip}?token=${token}`, { signal: options.signal }); const data = await res.json(); if (!res.ok || data?.error) { return null; } const [lat, lon] = (nn(data.loc) || "").split(","); const loc = nn(lat) && nn(lon) ? `${lat},${lon}` : null; return { source: "ipinfo", timezone: nn(data.timezone), continent: null, country: nn(data.country), region: nn(data.region), postal: nn(data.postal), city: nn(data.city), loc, org: nn(data.org), isp: nn(data.org), flag: null }; } async function _ipgeolocation(key, ip, options = {}) { const res = await fetch(`https://api.ipgeolocation.io/ipgeo?apiKey=${key}&ip=${ip}`, { signal: options.signal }); const data = await res.json(); if (!res.ok || data?.message) { return null; } const lon = nn(data.longitude); const lat = nn(data.latitude); const loc = lat != null && lon != null ? `${lat},${lon}` : null; return { source: "ipgeolocation", timezone: nn(data.time_zone?.name), continent: nn(data.continent_name), country: nn(data.country_name), region: nn(data.state_prov), postal: nn(data.zipcode), city: nn(data.city), loc, org: nn(data.organization), isp: nn(data.isp), flag: nn(data.country_flag_emoji) }; } async function _revGoogle(lat, lon, options = {}) { const url = `https://maps.googleapis.com/maps/api/geocode/json?latlng=${lat},${lon}&key=${_classPrivateFieldGet(_googleMapsKey, this)}`; const res = await fetch(url, { signal: options.signal }); const j = await res.json(); if (j.status !== "OK" || !j.results?.length) { return null; } const top = j.results[0]; return { components: top.address_components, formatted: top.formatted_address, provider: "google" }; } async function _revMapbox(lat, lon, options = {}) { const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${lon},${lat}.json?access_token=${_classPrivateFieldGet(_mapboxToken, this)}&limit=1`; const res = await fetch(url, { signal: options.signal }); const j = await res.json(); const f = j.features?.[0]; if (!f) { return null; } return { formatted: f.place_name, components: f.context, provider: "mapbox" }; } async function _revNominatim(lat, lon, options = {}) { const url = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lon}&addressdetails=1`; const res = await fetch(url, { headers: { "User-Agent": _classPrivateFieldGet(_nominatimUA, this) }, signal: options.signal }); const j = await res.json(); if (!j?.display_name) { return null; } return { formatted: j.display_name, components: j.address, provider: "nominatim" }; } ; module.exports = GeoTide;