about-system 0.10.10 → 0.10.11

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.
Files changed (56) hide show
  1. package/README.md +3 -1
  2. package/dist/about-system-cli.js +69 -54
  3. package/dist/about-system-cli.js.map +1 -1
  4. package/dist/index.d.ts +64 -18
  5. package/dist/index.js +1 -1
  6. package/dist/system-info-api-D85U-_0W.js +2473 -0
  7. package/dist/system-info-api-D85U-_0W.js.map +1 -0
  8. package/dist/system-info-api.d.ts +432 -62
  9. package/dist/system-info-api.js +1 -1
  10. package/dist/web/assets/index--HOsj3jw.js +14 -0
  11. package/dist/web/assets/index-BU_blZyu.css +1 -0
  12. package/dist/web/index.html +13 -0
  13. package/package.json +1 -1
  14. package/src/about-system-cli.ts +42 -3
  15. package/src/cache/cache-config.ts +3 -3
  16. package/src/info/network.ts +14 -47
  17. package/src/system-info-api.test.ts +106 -0
  18. package/src/system-info-api.ts +67 -28
  19. package/src/systeminfo-types.d.ts +6 -0
  20. package/src/utils/network.ts +113 -94
  21. package/src/utils/utils.test.ts +84 -75
  22. package/dist/about-system-cli.d.ts.map +0 -1
  23. package/dist/cache/cache-config.d.ts +0 -62
  24. package/dist/cache/cache-config.d.ts.map +0 -1
  25. package/dist/cache/cache.d.ts +0 -53
  26. package/dist/cache/cache.d.ts.map +0 -1
  27. package/dist/index.d.ts.map +0 -1
  28. package/dist/info/hardware.d.ts +0 -76
  29. package/dist/info/hardware.d.ts.map +0 -1
  30. package/dist/info/memory.d.ts +0 -59
  31. package/dist/info/memory.d.ts.map +0 -1
  32. package/dist/info/network.d.ts +0 -54
  33. package/dist/info/network.d.ts.map +0 -1
  34. package/dist/info/platform.d.ts +0 -36
  35. package/dist/info/platform.d.ts.map +0 -1
  36. package/dist/info/process.d.ts +0 -23
  37. package/dist/info/process.d.ts.map +0 -1
  38. package/dist/info/settings.d.ts +0 -185
  39. package/dist/info/settings.d.ts.map +0 -1
  40. package/dist/info/software.d.ts +0 -33
  41. package/dist/info/software.d.ts.map +0 -1
  42. package/dist/info/system-status.d.ts +0 -36
  43. package/dist/info/system-status.d.ts.map +0 -1
  44. package/dist/system-info-api-ByXoHI0m.js +0 -2464
  45. package/dist/system-info-api-ByXoHI0m.js.map +0 -1
  46. package/dist/system-info-api.d.ts.map +0 -1
  47. package/dist/types/internal-types.d.ts +0 -14
  48. package/dist/types/internal-types.d.ts.map +0 -1
  49. package/dist/utils/command.d.ts +0 -18
  50. package/dist/utils/command.d.ts.map +0 -1
  51. package/dist/utils/network.d.ts +0 -27
  52. package/dist/utils/network.d.ts.map +0 -1
  53. package/dist/utils/platform.d.ts +0 -11
  54. package/dist/utils/platform.d.ts.map +0 -1
  55. package/dist/web-server.d.ts +0 -21
  56. package/dist/web-server.d.ts.map +0 -1
@@ -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
- * Fetches IP geolocation information from ipinfo.io lite API
41
- * @param {string} token - IPInfo.io API token
42
- * @param {number} timeout - Request timeout in milliseconds
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
- async function fetchFromIPInfo(
46
- token: string = DEFAULT_IPINFO_TOKEN,
47
- timeout: number = DEFAULT_NETWORK_TIMEOUT
48
- ): Promise<IPInfo> {
46
+ function getJSON(
47
+ url: string,
48
+ timeout: number,
49
+ signal?: AbortSignal
50
+ ): Promise<any> {
49
51
  return new Promise((resolve) => {
50
- const url = `https://api.ipinfo.io/lite/8.8.8.8?token=${token}`;
52
+ if (signal?.aborted) return resolve(null);
51
53
 
52
- const req = https.get(url, (res) => {
53
- let data = "";
54
- res.on("data", (chunk) => (data += chunk));
55
- res.on("end", () => {
56
- try {
57
- const parsed = JSON.parse(data);
58
- // ipinfo lite returns: { ip, city, region, country, org }
59
- // Map to our standard IPInfo format
60
- resolve({
61
- ip: parsed.ip,
62
- city: parsed.city,
63
- hostname: parsed.hostname,
64
- org: parsed.org,
65
- });
66
- } catch {
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
- req.on("error", () => resolve({}));
73
- req.setTimeout(timeout, () => {
74
- req.destroy();
75
- resolve({});
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 geolocation information from ip-api.com (fallback)
82
- * @param {number} timeout - Request timeout in milliseconds
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 fetchFromIPAPI(
86
- timeout: number = DEFAULT_NETWORK_TIMEOUT
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
- return new Promise((resolve) => {
89
- const url = "http://ip-api.com/json/?fields=status,message,query,city,isp,org,as";
90
-
91
- const req = https.get(url, (res) => {
92
- let data = "";
93
- res.on("data", (chunk) => (data += chunk));
94
- res.on("end", () => {
95
- try {
96
- const parsed: IPAPIInfo = JSON.parse(data);
97
- if (parsed.status === "success") {
98
- // Map ip-api response to our standard IPInfo format
99
- resolve({
100
- ip: parsed.query,
101
- city: parsed.city,
102
- hostname: undefined,
103
- org: parsed.org || parsed.isp || parsed.as,
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
- req.on("error", () => resolve({}));
115
- req.setTimeout(timeout, () => {
116
- req.destroy();
117
- resolve({});
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 geolocation information using race between ipinfo.io and ip-api.com
124
- * Uses ipinfo.io lite endpoint first, falls back to ip-api.com if slower or fails
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 - Request timeout in milliseconds
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
- // Race both services, use whichever responds first
134
- const [ipinfoResult, ipapiResult] = await Promise.allSettled([
135
- fetchFromIPInfo(token, timeout),
136
- fetchFromIPAPI(timeout),
160
+ const [ipinfo, ipapi] = await Promise.all([
161
+ fetchFromIPInfo(token, timeout, signal),
162
+ fetchFromIPAPI(timeout, signal),
137
163
  ]);
138
164
 
139
- // Prefer ipinfo.io result if successful and has data
140
- if (ipinfoResult.status === "fulfilled" && ipinfoResult.value && ipinfoResult.value.ip) {
141
- return ipinfoResult.value;
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
- // Fall back to ip-api.com result if successful and has data
145
- if (ipapiResult.status === "fulfilled" && ipapiResult.value && ipapiResult.value.ip) {
146
- return ipapiResult.value;
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
- if (ipapiResult.status === "fulfilled" && ipapiResult.value) {
154
- return ipapiResult.value;
155
- }
156
-
157
- return {};
158
- }
176
+ return merged;
177
+ }
@@ -22,6 +22,11 @@ vi.mock("https", () => ({
22
22
  get: httpsGet,
23
23
  default: { get: httpsGet },
24
24
  }));
25
+ const httpGet = vi.hoisted(() => vi.fn());
26
+ vi.mock("http", () => ({
27
+ get: httpGet,
28
+ default: { get: httpGet },
29
+ }));
25
30
 
26
31
  import { commandExists, execCommand } from "./command";
27
32
  import { IS_LINUX, IS_MAC, IS_WINDOWS } from "./platform";
@@ -31,7 +36,9 @@ import { DEFAULT_IPINFO_TOKEN, DEFAULT_NETWORK_TIMEOUT } from "../cache/cache-co
31
36
  afterEach(() => {
32
37
  execSync.mockReset();
33
38
  httpsGet.mockReset();
39
+ httpGet.mockReset();
34
40
  vi.restoreAllMocks();
41
+ vi.useRealTimers();
35
42
  });
36
43
 
37
44
  describe("platform flags", () => {
@@ -118,24 +125,27 @@ describe("commandExists", () => {
118
125
  });
119
126
  });
120
127
 
121
- /** A fake `https.get` whose response body and failure mode a test controls. */
122
- function stubHttpsGet(behavior: (req: EventEmitter & { setTimeout: (ms: number, fn: () => void) => void; destroy: () => void }, onResponse: (res: EventEmitter) => void) => void) {
123
- httpsGet.mockImplementation(
124
- (_url: string, callback: (res: EventEmitter) => void) => {
125
- const req = Object.assign(new EventEmitter(), {
126
- setTimeout: vi.fn(),
127
- destroy: vi.fn(),
128
- });
129
- queueMicrotask(() => behavior(req, callback));
130
- return req;
131
- },
132
- );
133
- return httpsGet;
128
+ type FakeReq = EventEmitter & { destroy: () => void };
129
+
130
+ /**
131
+ * A fake `get` (https or http) whose response a test controls. `behavior`
132
+ * runs on the next microtask; leave it empty to simulate a hung request.
133
+ */
134
+ function stubGet(
135
+ get: typeof httpsGet,
136
+ behavior: (req: FakeReq, onResponse: (res: EventEmitter) => void) => void = () => {},
137
+ ) {
138
+ get.mockImplementation((_url: string, callback: (res: EventEmitter) => void) => {
139
+ const req: FakeReq = Object.assign(new EventEmitter(), { destroy: vi.fn() });
140
+ queueMicrotask(() => behavior(req, callback));
141
+ return req;
142
+ });
143
+ return get;
134
144
  }
135
145
 
136
146
  /** Emits a complete response body on the next tick. */
137
- const respondWith = (body: string) =>
138
- stubHttpsGet((_req, onResponse) => {
147
+ const respondWith = (get: typeof httpsGet, body: string) =>
148
+ stubGet(get, (_req, onResponse) => {
139
149
  const res = new EventEmitter();
140
150
  onResponse(res);
141
151
  res.emit("data", body);
@@ -144,57 +154,61 @@ const respondWith = (body: string) =>
144
154
 
145
155
  describe("fetchFromIPInfo", () => {
146
156
  it("parses the ipinfo.io lite payload", async () => {
147
- respondWith(JSON.stringify({ ip: "1.2.3.4", city: "San Francisco", org: "AS123 Test ISP" }));
157
+ respondWith(httpsGet, JSON.stringify({ ip: "1.2.3.4", asn: "AS123", as_name: "Test ISP", country: "US" }));
148
158
 
149
159
  await expect(fetchFromIPInfo()).resolves.toEqual({
150
160
  ip: "1.2.3.4",
151
- city: "San Francisco",
152
161
  org: "AS123 Test ISP",
153
162
  });
154
163
  });
155
164
 
156
- it("sends request to ipinfo.io lite endpoint with token", async () => {
157
- const get = respondWith("{}");
165
+ it("asks about this machine, not a fixed address", async () => {
166
+ const get = respondWith(httpsGet, "{}");
158
167
  await fetchFromIPInfo();
159
168
 
160
169
  expect(get.mock.calls[0][0]).toBe(
161
- `https://api.ipinfo.io/lite/8.8.8.8?token=${DEFAULT_IPINFO_TOKEN}`,
170
+ `https://api.ipinfo.io/lite/me?token=${DEFAULT_IPINFO_TOKEN}`,
162
171
  );
163
172
  });
164
173
 
165
174
  it("resolves empty rather than rejecting on a malformed body", async () => {
166
- respondWith("<html>gateway error</html>");
175
+ respondWith(httpsGet, "<html>gateway error</html>");
167
176
  await expect(fetchFromIPInfo()).resolves.toEqual({});
168
177
  });
169
178
 
170
179
  it("resolves empty rather than rejecting when the request errors", async () => {
171
- stubHttpsGet((req) => req.emit("error", new Error("ENOTFOUND")));
180
+ stubGet(httpsGet, (req) => req.emit("error", new Error("ENOTFOUND")));
172
181
  await expect(fetchFromIPInfo()).resolves.toEqual({});
173
182
  });
174
183
 
175
- it("arms a timeout that abandons the request", async () => {
176
- let armed: { ms: number; fire: () => void } | undefined;
177
- httpsGet.mockImplementation(() => {
178
- const req = Object.assign(new EventEmitter(), {
179
- setTimeout: (ms: number, fire: () => void) => {
180
- armed = { ms, fire };
181
- },
182
- destroy: vi.fn(),
183
- });
184
- return req;
185
- });
184
+ it("gives up after a hard 3 second deadline by default", async () => {
185
+ vi.useFakeTimers();
186
+ stubGet(httpsGet);
187
+ let done = false;
188
+ const pending = fetchFromIPInfo().then((r) => ((done = true), r));
186
189
 
187
- const pending = fetchFromIPInfo(DEFAULT_IPINFO_TOKEN, 1234);
188
- expect(armed?.ms).toBe(1234);
190
+ await vi.advanceTimersByTimeAsync(DEFAULT_NETWORK_TIMEOUT - 1);
191
+ expect(done).toBe(false);
192
+ await vi.advanceTimersByTimeAsync(1);
193
+ await expect(pending).resolves.toEqual({});
194
+ expect(DEFAULT_NETWORK_TIMEOUT).toBe(3000);
195
+ expect(httpsGet.mock.results[0].value.destroy).toHaveBeenCalled();
196
+ });
197
+
198
+ it("abandons the request when the signal aborts", async () => {
199
+ stubGet(httpsGet);
200
+ const controller = new AbortController();
201
+ const pending = fetchFromIPInfo(DEFAULT_IPINFO_TOKEN, 60_000, controller.signal);
189
202
 
190
- armed?.fire();
203
+ controller.abort();
191
204
  await expect(pending).resolves.toEqual({});
205
+ expect(httpsGet.mock.results[0].value.destroy).toHaveBeenCalled();
192
206
  });
193
207
  });
194
208
 
195
209
  describe("fetchFromIPAPI", () => {
196
210
  it("parses the ip-api.com payload", async () => {
197
- respondWith(JSON.stringify({
211
+ respondWith(httpGet, JSON.stringify({
198
212
  status: "success",
199
213
  query: "1.2.3.4",
200
214
  city: "San Francisco",
@@ -210,66 +224,51 @@ describe("fetchFromIPAPI", () => {
210
224
  });
211
225
  });
212
226
 
213
- it("sends request to ip-api.com endpoint", async () => {
214
- const get = respondWith('{"status":"success"}');
227
+ it("uses plain http, which is all ip-api.com's free tier serves", async () => {
228
+ const get = respondWith(httpGet, '{"status":"success"}');
215
229
  await fetchFromIPAPI();
216
230
 
217
231
  expect(get.mock.calls[0][0]).toBe(
218
232
  "http://ip-api.com/json/?fields=status,message,query,city,isp,org,as"
219
233
  );
234
+ expect(httpsGet).not.toHaveBeenCalled();
220
235
  });
221
236
 
222
237
  it("returns empty object on failed status", async () => {
223
- respondWith(JSON.stringify({ status: "fail", message: "private range" }));
238
+ respondWith(httpGet, JSON.stringify({ status: "fail", message: "private range" }));
224
239
  await expect(fetchFromIPAPI()).resolves.toEqual({});
225
240
  });
226
241
 
227
242
  it("resolves empty rather than rejecting on a malformed body", async () => {
228
- respondWith("<html>gateway error</html>");
243
+ respondWith(httpGet, "<html>gateway error</html>");
229
244
  await expect(fetchFromIPAPI()).resolves.toEqual({});
230
245
  });
231
246
  });
232
247
 
233
- describe("fetchIPInfo (race)", () => {
234
- it("returns ipinfo result when it responds first with data", async () => {
235
- let ipinfoResolve: (value: any) => void;
236
- let ipapiResolve: (value: any) => void;
237
-
238
- const ipinfoPromise = new Promise((resolve) => { ipinfoResolve = resolve; });
239
- const ipapiPromise = new Promise((resolve) => { ipapiResolve = resolve; });
240
-
241
- vi.spyOn(await import("./network"), "fetchFromIPInfo").mockReturnValue(ipinfoPromise);
242
- vi.spyOn(await import("./network"), "fetchFromIPAPI").mockReturnValue(ipapiPromise);
243
-
244
- const resultPromise = fetchIPInfo();
248
+ describe("fetchIPInfo", () => {
249
+ it("prefers ipinfo per field and fills gaps from ip-api", async () => {
250
+ respondWith(httpsGet, JSON.stringify({ ip: "1.2.3.4", asn: "AS123", as_name: "Primary ISP" }));
251
+ respondWith(httpGet, JSON.stringify({ status: "success", query: "5.6.7.8", city: "New York", org: "AS456 Other" }));
245
252
 
246
- // Resolve ipinfo first with data
247
- ipinfoResolve!({ ip: "1.2.3.4", city: "San Francisco" });
248
- ipapiResolve!({ ip: "5.6.7.8", city: "New York" });
249
-
250
- await expect(resultPromise).resolves.toEqual({
253
+ await expect(fetchIPInfo()).resolves.toEqual({
251
254
  ip: "1.2.3.4",
252
- city: "San Francisco",
255
+ city: "New York",
256
+ org: "AS123 Primary ISP",
253
257
  });
254
258
  });
255
259
 
256
- it("falls back to ip-api when ipinfo fails", async () => {
257
- let ipinfoResolve: (value: any) => void;
258
- let ipapiResolve: (value: any) => void;
259
-
260
- const ipinfoPromise = new Promise((resolve) => { ipinfoResolve = resolve; });
261
- const ipapiPromise = new Promise((resolve) => { ipapiResolve = resolve; });
262
-
263
- vi.spyOn(await import("./network"), "fetchFromIPInfo").mockReturnValue(ipinfoPromise);
264
- vi.spyOn(await import("./network"), "fetchFromIPAPI").mockReturnValue(ipapiPromise);
260
+ it("prefers an IPv4 address over ipinfo's IPv6 one", async () => {
261
+ respondWith(httpsGet, JSON.stringify({ ip: "2600:1010::1", asn: "AS123", as_name: "Primary ISP" }));
262
+ respondWith(httpGet, JSON.stringify({ status: "success", query: "5.6.7.8", city: "New York" }));
265
263
 
266
- const resultPromise = fetchIPInfo();
264
+ await expect(fetchIPInfo()).resolves.toMatchObject({ ip: "5.6.7.8" });
265
+ });
267
266
 
268
- // Resolve ipinfo with empty, ipapi with data
269
- ipinfoResolve!({});
270
- ipapiResolve!({ ip: "5.6.7.8", city: "New York", org: "AS456 Fallback ISP" });
267
+ it("falls back to ip-api when ipinfo fails", async () => {
268
+ stubGet(httpsGet, (req) => req.emit("error", new Error("ECONNRESET")));
269
+ respondWith(httpGet, JSON.stringify({ status: "success", query: "5.6.7.8", city: "New York", org: "AS456 Fallback ISP" }));
271
270
 
272
- await expect(resultPromise).resolves.toEqual({
271
+ await expect(fetchIPInfo()).resolves.toEqual({
273
272
  ip: "5.6.7.8",
274
273
  city: "New York",
275
274
  org: "AS456 Fallback ISP",
@@ -277,9 +276,19 @@ describe("fetchIPInfo (race)", () => {
277
276
  });
278
277
 
279
278
  it("returns empty when both fail", async () => {
280
- vi.spyOn(await import("./network"), "fetchFromIPInfo").mockResolvedValue({});
281
- vi.spyOn(await import("./network"), "fetchFromIPAPI").mockResolvedValue({});
279
+ stubGet(httpsGet, (req) => req.emit("error", new Error("ENOTFOUND")));
280
+ stubGet(httpGet, (req) => req.emit("error", new Error("ENOTFOUND")));
282
281
 
283
282
  await expect(fetchIPInfo()).resolves.toEqual({});
284
283
  });
284
+
285
+ it("never waits past the shared deadline when both hang", async () => {
286
+ vi.useFakeTimers();
287
+ stubGet(httpsGet);
288
+ stubGet(httpGet);
289
+ const pending = fetchIPInfo();
290
+
291
+ await vi.advanceTimersByTimeAsync(DEFAULT_NETWORK_TIMEOUT);
292
+ await expect(pending).resolves.toEqual({});
293
+ });
285
294
  });
@@ -1 +0,0 @@
1
- {"version":3,"file":"about-system-cli.d.ts","sourceRoot":"","sources":["../src/about-system-cli.ts"],"names":[],"mappings":";AAoJA,iBAAe,iBAAiB,CAC9B,kBAAkB,GAAE,MAAM,EAAE,EAAE,GAAG,IAAW,GAC3C,OAAO,CAAC,IAAI,CAAC,CAqDf;AA8ED,iBAAS,oBAAoB,IAAI,IAAI,CAoGpC;AAoJD,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,CAAC"}
@@ -1,62 +0,0 @@
1
- /**
2
- * Cache configuration and constants
3
- * @module cache-config
4
- */
5
- /**
6
- * Cache file location in system's temporary directory
7
- * @constant
8
- */
9
- export declare const CACHE_FILE: string;
10
- /**
11
- * Cache duration configuration for different system information types
12
- * Values are in milliseconds
13
- *
14
- * @constant
15
- * @property {number} ip - IP information cache (10 minutes)
16
- * @property {number} cpu - CPU information cache (24 hours)
17
- * @property {number} gpu - GPU information cache (24 hours)
18
- * @property {number} os - OS information cache (24 hours)
19
- * @property {number} device - Device information cache (24 hours)
20
- * @property {number} kernel - Kernel information cache (1 hour)
21
- * @property {number} pacman - Package managers cache (10 minutes)
22
- * @property {number} ports - Open ports cache (5 minutes)
23
- * @property {number} containers - Docker containers cache (5 minutes)
24
- * @property {number} top_process - Top process cache (5 seconds)
25
- * @property {number} disk_used - Disk usage cache (1 minute)
26
- * @property {number} ram_used - RAM usage cache (10 seconds)
27
- * @property {number} services_running - Services cache (5 minutes)
28
- * @property {number} temperature - Temperature cache (30 seconds)
29
- * @property {number} battery - Battery status cache (1 minute)
30
- * @property {number} network_interfaces - Network interfaces cache (5 minutes)
31
- * @property {number} mount_points - Mount points cache (10 minutes)
32
- */
33
- export declare const CACHE_DURATION: {
34
- ip: number;
35
- cpu: number;
36
- gpu: number;
37
- os: number;
38
- device: number;
39
- kernel: number;
40
- pacman: number;
41
- ports: number;
42
- containers: number;
43
- top_process: number;
44
- disk_used: number;
45
- ram_used: number;
46
- services_running: number;
47
- temperature: number;
48
- battery: number;
49
- network_interfaces: number;
50
- mount_points: number;
51
- };
52
- /**
53
- * Default IPInfo.io API token for geolocation
54
- * @constant
55
- */
56
- export declare const DEFAULT_IPINFO_TOKEN = "da2d6cc4baa5d1";
57
- /**
58
- * Default network request timeout in milliseconds
59
- * @constant
60
- */
61
- export declare const DEFAULT_NETWORK_TIMEOUT = 5000;
62
- //# sourceMappingURL=cache-config.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache-config.d.ts","sourceRoot":"","sources":["../../src/cache/cache-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;GAGG;AACH,eAAO,MAAM,UAAU,QAAkD,CAAC;AAE1E;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;CAkB1B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,oBAAoB,mBAAmB,CAAC;AAErD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC"}
@@ -1,53 +0,0 @@
1
- /**
2
- * Cache management utilities
3
- * @module cache
4
- */
5
- /**
6
- * Represents a cached value with timestamp
7
- * @interface CacheEntry
8
- */
9
- export interface CacheEntry {
10
- /** The cached value */
11
- value: any;
12
- /** Unix timestamp when the value was cached */
13
- timestamp: number;
14
- }
15
- /**
16
- * Cache storage structure
17
- * @interface Cache
18
- */
19
- export interface Cache {
20
- [key: string]: CacheEntry;
21
- }
22
- /**
23
- * Loads cache from disk
24
- * @returns {Cache} Cached data or empty object if cache doesn't exist or is corrupted
25
- */
26
- export declare function loadCache(): Cache;
27
- /**
28
- * Saves cache to disk
29
- * @param {Cache} cache - Cache object to save
30
- */
31
- export declare function saveCache(cache: Cache): void;
32
- /**
33
- * Checks if a cache entry is still valid based on configured duration
34
- * @param {CacheEntry} cacheEntry - The cache entry to validate
35
- * @param {string} key - The key name to determine cache duration
36
- * @returns {boolean} True if cache is still valid
37
- */
38
- export declare function isCacheValid(cacheEntry: CacheEntry, key: string): boolean;
39
- /**
40
- * Retrieves a value from cache if valid
41
- * @param {Cache} cache - Cache object
42
- * @param {string} key - Key to retrieve
43
- * @returns {any} Cached value or null if not found/expired
44
- */
45
- export declare function getCachedValue(cache: Cache, key: string): any;
46
- /**
47
- * Stores a value in cache with current timestamp
48
- * @param {Cache} cache - Cache object
49
- * @param {string} key - Key to store under
50
- * @param {any} value - Value to cache
51
- */
52
- export declare function setCachedValue(cache: Cache, key: string, value: any): void;
53
- //# sourceMappingURL=cache.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/cache/cache.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,uBAAuB;IACvB,KAAK,EAAE,GAAG,CAAC;IACX,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B;AAED;;;GAGG;AACH,wBAAgB,SAAS,IAAI,KAAK,CASjC;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAM5C;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAKzE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,GAAG,GAAG,CAQ7D;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,GAAG,IAAI,CAK1E"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAC3E,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,QAAQ,EAAE,qBAAqB,EAAE,yBAAyB,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC"}