about-system 0.10.10 → 0.10.12
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 +5 -3
- package/dist/about-system-cli.js +69 -54
- package/dist/about-system-cli.js.map +1 -1
- package/dist/index.d.ts +64 -18
- package/dist/index.js +1 -1
- package/dist/system-info-api-7iy1ZV2o.js +2475 -0
- package/dist/system-info-api-7iy1ZV2o.js.map +1 -0
- package/dist/system-info-api.d.ts +432 -62
- package/dist/system-info-api.js +1 -1
- package/dist/web/assets/index--HOsj3jw.js +14 -0
- package/dist/web/assets/index-BU_blZyu.css +1 -0
- package/dist/web/index.html +13 -0
- package/package.json +1 -1
- package/src/about-system-cli.ts +42 -3
- package/src/cache/cache-config.ts +3 -3
- package/src/info/hardware.test.ts +27 -12
- package/src/info/hardware.ts +25 -22
- package/src/info/network.ts +14 -47
- package/src/system-info-api.test.ts +106 -0
- package/src/system-info-api.ts +67 -28
- package/src/systeminfo-types.d.ts +16 -10
- package/src/utils/network.ts +113 -94
- package/src/utils/utils.test.ts +84 -75
- package/dist/about-system-cli.d.ts.map +0 -1
- package/dist/cache/cache-config.d.ts +0 -62
- package/dist/cache/cache-config.d.ts.map +0 -1
- package/dist/cache/cache.d.ts +0 -53
- package/dist/cache/cache.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/info/hardware.d.ts +0 -76
- package/dist/info/hardware.d.ts.map +0 -1
- package/dist/info/memory.d.ts +0 -59
- package/dist/info/memory.d.ts.map +0 -1
- package/dist/info/network.d.ts +0 -54
- package/dist/info/network.d.ts.map +0 -1
- package/dist/info/platform.d.ts +0 -36
- package/dist/info/platform.d.ts.map +0 -1
- package/dist/info/process.d.ts +0 -23
- package/dist/info/process.d.ts.map +0 -1
- package/dist/info/settings.d.ts +0 -185
- package/dist/info/settings.d.ts.map +0 -1
- package/dist/info/software.d.ts +0 -33
- package/dist/info/software.d.ts.map +0 -1
- package/dist/info/system-status.d.ts +0 -36
- package/dist/info/system-status.d.ts.map +0 -1
- package/dist/system-info-api-ByXoHI0m.js +0 -2464
- package/dist/system-info-api-ByXoHI0m.js.map +0 -1
- package/dist/system-info-api.d.ts.map +0 -1
- package/dist/types/internal-types.d.ts +0 -14
- package/dist/types/internal-types.d.ts.map +0 -1
- package/dist/utils/command.d.ts +0 -18
- package/dist/utils/command.d.ts.map +0 -1
- package/dist/utils/network.d.ts +0 -27
- package/dist/utils/network.d.ts.map +0 -1
- package/dist/utils/platform.d.ts +0 -11
- package/dist/utils/platform.d.ts.map +0 -1
- package/dist/web-server.d.ts +0 -21
- package/dist/web-server.d.ts.map +0 -1
package/src/system-info-api.ts
CHANGED
|
@@ -12,8 +12,11 @@
|
|
|
12
12
|
import os from "os";
|
|
13
13
|
import fs from "fs";
|
|
14
14
|
import { fetchIPInfo } from "./utils/network";
|
|
15
|
+
import type { IPInfo } from "./utils/network";
|
|
15
16
|
import path from "path";
|
|
16
17
|
import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
|
|
18
|
+
import type { Cache } from "./cache/cache";
|
|
19
|
+
import { isCacheValid } from "./cache/cache";
|
|
17
20
|
import { CACHE_FILE } from "./info/settings"; // Using CACHE_FILE from settings to ensure consistency
|
|
18
21
|
|
|
19
22
|
// Import info functions from modules
|
|
@@ -159,34 +162,78 @@ export const infoFunctions = {
|
|
|
159
162
|
screen_resolution,
|
|
160
163
|
};
|
|
161
164
|
|
|
165
|
+
/** Blocks derived from the public IP lookup. */
|
|
166
|
+
const IP_BLOCKS = new Set(["ip", "city", "domain", "isp"]);
|
|
167
|
+
|
|
168
|
+
/** Lets pending I/O callbacks (keypresses, HTTP responses) run between blocks. */
|
|
169
|
+
const yieldToEventLoop = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
170
|
+
|
|
162
171
|
/**
|
|
163
|
-
*
|
|
172
|
+
* Returns the cached public IP lookup, or fetches it when the cache entry is
|
|
173
|
+
* older than 10 minutes. A failed fetch keeps the previous value and is still
|
|
174
|
+
* timestamped, so an offline machine is not re-queried on every launch. An
|
|
175
|
+
* aborted fetch leaves the cache untouched.
|
|
176
|
+
*/
|
|
177
|
+
async function getIPInfo(cache: Cache, signal?: AbortSignal): Promise<IPInfo> {
|
|
178
|
+
const entry = cache["ipInfo"];
|
|
179
|
+
if (entry && isCacheValid(entry, "ipInfo")) return entry.value || {};
|
|
180
|
+
|
|
181
|
+
const fresh = await fetchIPInfo(undefined, undefined, signal);
|
|
182
|
+
const value = fresh.ip ? fresh : entry?.value || {};
|
|
183
|
+
if (!signal?.aborted) {
|
|
184
|
+
cache["ipInfo"] = { value, timestamp: Date.now() };
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get system information as a clean JSON object.
|
|
191
|
+
*
|
|
192
|
+
* `options.keys` limits collection to those blocks (all by default).
|
|
193
|
+
* `options.signal` stops collection early: whatever was gathered before the
|
|
194
|
+
* abort is returned, and the remaining blocks are left out.
|
|
164
195
|
*/
|
|
165
196
|
export async function getSystemInfo(
|
|
166
197
|
options: SystemInfoOptions = {}
|
|
167
198
|
): Promise<SystemInfo> {
|
|
199
|
+
const { keys, signal } = options;
|
|
168
200
|
const cache = loadCache();
|
|
169
201
|
const context: InfoContext = { cache };
|
|
170
202
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
203
|
+
const wanted = Object.keys(infoFunctions).filter(
|
|
204
|
+
(key) => !keys || keys.includes(key)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Start the IP lookup first so it overlaps with the local blocks.
|
|
208
|
+
const ipInfoPromise = wanted.some((key) => IP_BLOCKS.has(key))
|
|
209
|
+
? getIPInfo(cache, signal)
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
const collected: Record<string, unknown> = {};
|
|
213
|
+
const run = async (key: string) => {
|
|
214
|
+
try {
|
|
215
|
+
collected[key] = await infoFunctions[key as keyof typeof infoFunctions](context);
|
|
216
|
+
} catch {
|
|
217
|
+
collected[key] = "";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
for (const key of wanted) {
|
|
222
|
+
if (IP_BLOCKS.has(key)) continue;
|
|
223
|
+
await yieldToEventLoop();
|
|
224
|
+
if (signal?.aborted) break;
|
|
225
|
+
await run(key);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// On abort the lookup resolves at once with whatever was cached.
|
|
229
|
+
if (ipInfoPromise) {
|
|
230
|
+
context.ipInfo = await ipInfoPromise;
|
|
231
|
+
for (const key of wanted) {
|
|
232
|
+
if (IP_BLOCKS.has(key)) await run(key);
|
|
233
|
+
}
|
|
187
234
|
}
|
|
188
235
|
|
|
189
|
-
//
|
|
236
|
+
// Keep the infoFunctions key order regardless of collection order.
|
|
190
237
|
const info: Partial<SystemInfo> = {
|
|
191
238
|
timestamp: new Date().toISOString(),
|
|
192
239
|
platform: IS_WINDOWS
|
|
@@ -197,16 +244,8 @@ export async function getSystemInfo(
|
|
|
197
244
|
? "linux"
|
|
198
245
|
: "unknown",
|
|
199
246
|
};
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (const [key, fn] of Object.entries(infoFunctions)) {
|
|
203
|
-
try {
|
|
204
|
-
// most functions are sync but some are async (ip related)
|
|
205
|
-
const value = await fn(context);
|
|
206
|
-
info[key as keyof SystemInfo] = value as any;
|
|
207
|
-
} catch (error) {
|
|
208
|
-
info[key as keyof SystemInfo] = "" as any;
|
|
209
|
-
}
|
|
247
|
+
for (const key of wanted) {
|
|
248
|
+
if (key in collected) info[key as keyof SystemInfo] = collected[key] as any;
|
|
210
249
|
}
|
|
211
250
|
|
|
212
251
|
saveCache(cache);
|
|
@@ -125,12 +125,12 @@ export interface SystemInfo {
|
|
|
125
125
|
/**
|
|
126
126
|
* CPU benchmark score from Geekbench 6 multi-core
|
|
127
127
|
* Shows performance score (in thousands) and ranking position
|
|
128
|
-
* Uses fuzzy matching to find similar CPU models from
|
|
128
|
+
* Uses fuzzy matching to find similar CPU models from Top 1000 list
|
|
129
129
|
* Empty string if CPU not found or benchmark data unavailable
|
|
130
130
|
*
|
|
131
131
|
* @example
|
|
132
|
-
* - "38k #1
|
|
133
|
-
* - "21k #68
|
|
132
|
+
* - "38k #1 Top 1%" (top performing CPU)
|
|
133
|
+
* - "21k #68 Top 7%" (mid-range CPU)
|
|
134
134
|
* - "" (CPU not in benchmark database)
|
|
135
135
|
*/
|
|
136
136
|
bench: string;
|
|
@@ -142,9 +142,9 @@ export interface SystemInfo {
|
|
|
142
142
|
* Empty string if CPU not found or benchmark data unavailable
|
|
143
143
|
*
|
|
144
144
|
* @example
|
|
145
|
-
* - "Geekbench 6: 38k (Rank #1,
|
|
146
|
-
* - "Geekbench 6: 21k (Rank #68,
|
|
147
|
-
* - "Geekbench 6: 19k (Rank #91,
|
|
145
|
+
* - "Geekbench 6: 38k (Rank #1, Top 1%) - ARM" (Apple M4 Ultra)
|
|
146
|
+
* - "Geekbench 6: 21k (Rank #68, Top 7%) - Intel" (Intel Core i9)
|
|
147
|
+
* - "Geekbench 6: 19k (Rank #91, Top 10%) - AMD" (AMD Ryzen 9 7950X)
|
|
148
148
|
* - "" (CPU not in benchmark database)
|
|
149
149
|
*/
|
|
150
150
|
cpu_bench_info: string;
|
|
@@ -156,8 +156,8 @@ export interface SystemInfo {
|
|
|
156
156
|
* Empty string if GPU not found or benchmark data unavailable
|
|
157
157
|
*
|
|
158
158
|
* @example
|
|
159
|
-
* - "38k #1
|
|
160
|
-
* - "21k #68
|
|
159
|
+
* - "38k #1 Top 1%" (top performing GPU)
|
|
160
|
+
* - "21k #68 Top 7%" (mid-range GPU)
|
|
161
161
|
* - "" (GPU not in benchmark database)
|
|
162
162
|
*/
|
|
163
163
|
gpu_bench: string;
|
|
@@ -169,8 +169,8 @@ export interface SystemInfo {
|
|
|
169
169
|
* Empty string if GPU not found or benchmark data unavailable
|
|
170
170
|
*
|
|
171
171
|
* @example
|
|
172
|
-
* - "Geekbench 6: 38k (Rank #1,
|
|
173
|
-
* - "Geekbench 6: 21k (Rank #68,
|
|
172
|
+
* - "Geekbench 6: 38k (Rank #1, Top 1%) - NVIDIA" (NVIDIA GPU)
|
|
173
|
+
* - "Geekbench 6: 21k (Rank #68, Top 7%) - AMD" (AMD GPU)
|
|
174
174
|
* - "" (GPU not in benchmark database)
|
|
175
175
|
*/
|
|
176
176
|
gpu_bench_info: string;
|
|
@@ -475,6 +475,12 @@ export interface SystemInfoOptions {
|
|
|
475
475
|
|
|
476
476
|
/** Return data object instead of printing to console (module usage) */
|
|
477
477
|
returnData?: boolean;
|
|
478
|
+
|
|
479
|
+
/** Only collect these blocks (e.g. ["cpu", "ip"]); all blocks when omitted */
|
|
480
|
+
keys?: string[];
|
|
481
|
+
|
|
482
|
+
/** Abort to stop collecting early and return only the blocks gathered so far */
|
|
483
|
+
signal?: AbortSignal;
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
/**
|
package/src/utils/network.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @module network
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import http from "http";
|
|
6
7
|
import https from "https";
|
|
7
8
|
import {
|
|
8
9
|
DEFAULT_IPINFO_TOKEN,
|
|
@@ -29,6 +30,7 @@ export interface IPInfo {
|
|
|
29
30
|
* @interface IPAPIInfo
|
|
30
31
|
*/
|
|
31
32
|
interface IPAPIInfo {
|
|
33
|
+
status?: string;
|
|
32
34
|
query?: string;
|
|
33
35
|
city?: string;
|
|
34
36
|
isp?: string;
|
|
@@ -37,122 +39,139 @@ interface IPAPIInfo {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
42
|
+
* GETs a URL and parses the body as JSON. Never rejects: resolves `null` on
|
|
43
|
+
* any error, on abort, or once `timeout` ms have passed in total — a hard
|
|
44
|
+
* deadline for the whole request, not a socket-idle timeout.
|
|
44
45
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
timeout: number
|
|
48
|
-
|
|
46
|
+
function getJSON(
|
|
47
|
+
url: string,
|
|
48
|
+
timeout: number,
|
|
49
|
+
signal?: AbortSignal
|
|
50
|
+
): Promise<any> {
|
|
49
51
|
return new Promise((resolve) => {
|
|
50
|
-
|
|
52
|
+
if (signal?.aborted) return resolve(null);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
resolve({});
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
});
|
|
54
|
+
let settled = false;
|
|
55
|
+
let req: http.ClientRequest | undefined;
|
|
56
|
+
const finish = (value: any) => {
|
|
57
|
+
if (settled) return;
|
|
58
|
+
settled = true;
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
signal?.removeEventListener("abort", onAbort);
|
|
61
|
+
resolve(value);
|
|
62
|
+
};
|
|
63
|
+
const onAbort = () => {
|
|
64
|
+
req?.destroy();
|
|
65
|
+
finish(null);
|
|
66
|
+
};
|
|
67
|
+
const timer = setTimeout(onAbort, timeout);
|
|
68
|
+
signal?.addEventListener("abort", onAbort);
|
|
71
69
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
req
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
try {
|
|
71
|
+
const get = url.startsWith("https:") ? https.get : http.get;
|
|
72
|
+
req = get(url, (res) => {
|
|
73
|
+
let data = "";
|
|
74
|
+
res.on("data", (chunk) => (data += chunk));
|
|
75
|
+
res.on("end", () => {
|
|
76
|
+
try {
|
|
77
|
+
finish(JSON.parse(data));
|
|
78
|
+
} catch {
|
|
79
|
+
finish(null);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
res.on("error", () => finish(null));
|
|
83
|
+
});
|
|
84
|
+
req.on("error", () => finish(null));
|
|
85
|
+
} catch {
|
|
86
|
+
finish(null);
|
|
87
|
+
}
|
|
77
88
|
});
|
|
78
89
|
}
|
|
79
90
|
|
|
80
91
|
/**
|
|
81
|
-
* Fetches IP
|
|
82
|
-
* @param {
|
|
92
|
+
* Fetches IP information for this machine from the ipinfo.io lite API
|
|
93
|
+
* @param {string} token - IPInfo.io API token
|
|
94
|
+
* @param {number} timeout - Total request deadline in milliseconds
|
|
95
|
+
* @param {AbortSignal} signal - Abandons the request when aborted
|
|
83
96
|
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
84
97
|
*/
|
|
85
|
-
async function
|
|
86
|
-
|
|
98
|
+
export async function fetchFromIPInfo(
|
|
99
|
+
token: string = DEFAULT_IPINFO_TOKEN,
|
|
100
|
+
timeout: number = DEFAULT_NETWORK_TIMEOUT,
|
|
101
|
+
signal?: AbortSignal
|
|
87
102
|
): Promise<IPInfo> {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
});
|
|
105
|
-
} else {
|
|
106
|
-
resolve({});
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
resolve({});
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
});
|
|
103
|
+
const parsed = await getJSON(
|
|
104
|
+
`https://api.ipinfo.io/lite/me?token=${token}`,
|
|
105
|
+
timeout,
|
|
106
|
+
signal
|
|
107
|
+
);
|
|
108
|
+
if (!parsed || typeof parsed !== "object") return {};
|
|
109
|
+
// ipinfo lite returns { ip, asn, as_name, country, ... } — no city.
|
|
110
|
+
const org =
|
|
111
|
+
parsed.org ?? (parsed.asn && parsed.as_name ? `${parsed.asn} ${parsed.as_name}` : undefined);
|
|
112
|
+
return {
|
|
113
|
+
ip: parsed.ip,
|
|
114
|
+
city: parsed.city,
|
|
115
|
+
hostname: parsed.hostname,
|
|
116
|
+
org,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
113
119
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
/**
|
|
121
|
+
* Fetches IP geolocation information from ip-api.com (fallback). The free
|
|
122
|
+
* tier is plain HTTP only.
|
|
123
|
+
* @param {number} timeout - Total request deadline in milliseconds
|
|
124
|
+
* @param {AbortSignal} signal - Abandons the request when aborted
|
|
125
|
+
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
126
|
+
*/
|
|
127
|
+
export async function fetchFromIPAPI(
|
|
128
|
+
timeout: number = DEFAULT_NETWORK_TIMEOUT,
|
|
129
|
+
signal?: AbortSignal
|
|
130
|
+
): Promise<IPInfo> {
|
|
131
|
+
const parsed: IPAPIInfo | null = await getJSON(
|
|
132
|
+
"http://ip-api.com/json/?fields=status,message,query,city,isp,org,as",
|
|
133
|
+
timeout,
|
|
134
|
+
signal
|
|
135
|
+
);
|
|
136
|
+
if (!parsed || parsed.status !== "success") return {};
|
|
137
|
+
return {
|
|
138
|
+
ip: parsed.query,
|
|
139
|
+
city: parsed.city,
|
|
140
|
+
hostname: undefined,
|
|
141
|
+
org: parsed.org || parsed.isp || parsed.as,
|
|
142
|
+
};
|
|
120
143
|
}
|
|
121
144
|
|
|
122
145
|
/**
|
|
123
|
-
* Fetches IP
|
|
124
|
-
*
|
|
146
|
+
* Fetches IP information from ipinfo.io and ip-api.com in parallel and merges
|
|
147
|
+
* them: ipinfo.io wins per field, ip-api.com fills the gaps (ipinfo lite has
|
|
148
|
+
* no city). Both requests share the same deadline, so this never takes longer
|
|
149
|
+
* than `timeout`.
|
|
125
150
|
* @param {string} token - IPInfo.io API token
|
|
126
|
-
* @param {number} timeout -
|
|
151
|
+
* @param {number} timeout - Total deadline in milliseconds
|
|
152
|
+
* @param {AbortSignal} signal - Abandons both requests when aborted
|
|
127
153
|
* @returns {Promise<IPInfo>} IP information object or empty object on error
|
|
128
154
|
*/
|
|
129
155
|
export async function fetchIPInfo(
|
|
130
156
|
token: string = DEFAULT_IPINFO_TOKEN,
|
|
131
|
-
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
157
|
+
timeout: number = DEFAULT_NETWORK_TIMEOUT,
|
|
158
|
+
signal?: AbortSignal
|
|
132
159
|
): Promise<IPInfo> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
fetchFromIPAPI(timeout),
|
|
160
|
+
const [ipinfo, ipapi] = await Promise.all([
|
|
161
|
+
fetchFromIPInfo(token, timeout, signal),
|
|
162
|
+
fetchFromIPAPI(timeout, signal),
|
|
137
163
|
]);
|
|
138
164
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
165
|
+
const merged: IPInfo = {};
|
|
166
|
+
for (const source of [ipapi, ipinfo]) {
|
|
167
|
+
for (const [key, value] of Object.entries(source)) {
|
|
168
|
+
if (value) merged[key as keyof IPInfo] = value;
|
|
169
|
+
}
|
|
142
170
|
}
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Return whichever has data, or empty object
|
|
150
|
-
if (ipinfoResult.status === "fulfilled" && ipinfoResult.value) {
|
|
151
|
-
return ipinfoResult.value;
|
|
171
|
+
// ipinfo answers over IPv6 when the machine has it; the short IPv4 address
|
|
172
|
+
// reads better in a one-line greeting, so keep ip-api's when it has one.
|
|
173
|
+
if (merged.ip?.includes(":") && ipapi.ip && !ipapi.ip.includes(":")) {
|
|
174
|
+
merged.ip = ipapi.ip;
|
|
152
175
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return {};
|
|
158
|
-
}
|
|
176
|
+
return merged;
|
|
177
|
+
}
|