Skip to content

Commit c18752c

Browse files
committed
added proxy health check, bug fix, ux improvements, added proxy lists, bump version 1.1.0
1 parent 0c9d833 commit c18752c

28 files changed

Lines changed: 4397 additions & 1035 deletions

package-lock.json

Lines changed: 1807 additions & 891 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
11
{
22
"name": "api-key-health-checker",
3-
"version": "1.0.2",
3+
"version": "1.1.0",
44
"description": "Desktop app to validate API keys for OpenAI, Gemini, YouTube, and custom endpoints with batch checks, rate limits, and reports.",
55
"license": "BSD-3-Clause",
66
"author": "nbox (https://github.com/nbox)",
77
"main": "dist/main/main.js",
88
"scripts": {
9-
"dev": "concurrently -k \"vite\" \"tsc -p tsconfig.main.json -w\" \"wait-on dist/main/main.js && electron .\"",
9+
"dev": "concurrently -k \"vite\" \"tsc -p tsconfig.main.json -w\" \"wait-on http://localhost:5173 dist/main/main.js && electron .\"",
1010
"build": "tsc -p tsconfig.main.json && vite build",
1111
"preview": "vite preview",
1212
"pack": "npm run build && electron-builder --dir",
1313
"dist": "node -e \"const fs=require('fs'); for (const p of ['dist','release']) { if (fs.existsSync(p)) fs.rmSync(p,{recursive:true,force:true}); }\" && npm run build && electron-builder"
1414
},
1515
"dependencies": {
16+
"http-proxy-agent": "^5.0.0",
1617
"iconv-lite": "^0.6.3",
18+
"https-proxy-agent": "^5.0.1",
1719
"react": "^18.2.0",
1820
"react-dom": "^18.2.0"
1921
},
@@ -24,12 +26,12 @@
2426
"@vitejs/plugin-react": "^4.2.1",
2527
"autoprefixer": "^10.4.17",
2628
"concurrently": "^8.2.2",
27-
"electron": "^30.0.1",
28-
"electron-builder": "^24.13.3",
29+
"electron": "^40.0.0",
30+
"electron-builder": "^26.6.0",
2931
"postcss": "^8.4.35",
3032
"tailwindcss": "^3.4.1",
3133
"typescript": "^5.4.2",
32-
"vite": "^5.1.4",
34+
"vite": "^6.4.1",
3335
"wait-on": "^7.2.0"
3436
},
3537
"build": {

src/main/engine/processManager.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export class ProcessManager {
129129
const run = new ProcessRun({
130130
id: processId,
131131
name: payload.name,
132+
serviceId: payload.serviceId,
132133
keys: payload.keys,
133134
method: payload.settings.method,
134135
settings: payload.settings,

src/main/engine/processRun.ts

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import type {
55
ProcessProgressEvent,
66
ProcessSettings,
77
ProcessStatus,
8-
CustomServiceConfig
8+
CustomServiceConfig,
9+
ServiceId
910
} from "../../shared/types";
1011
import { maskKey, sanitizeErrorMessage } from "../../shared/mask";
12+
import { decodeProxyKey } from "../../shared/proxy";
1113
import type { ServiceAdapter } from "../services/types";
1214
import { RateLimiter } from "./rateLimiter";
1315
import { randomBetween, sleep } from "./utils";
@@ -21,6 +23,7 @@ interface ProcessEvents {
2123
export interface ProcessRunOptions {
2224
id: string;
2325
name: string;
26+
serviceId: ServiceId;
2427
keys: string[];
2528
method: CheckMethod;
2629
settings: ProcessSettings;
@@ -40,6 +43,7 @@ export class ProcessRun {
4043
private customConfig?: CustomServiceConfig;
4144
private rateLimiter: RateLimiter;
4245
private events: ProcessEvents;
46+
private isProxyService: boolean;
4347
private status: ProcessStatus = "Running";
4448
private queueIndex = 0;
4549
private activeCount = 0;
@@ -52,6 +56,7 @@ export class ProcessRun {
5256
constructor(options: ProcessRunOptions) {
5357
this.id = options.id;
5458
this.name = options.name;
59+
this.isProxyService = options.serviceId === "proxy";
5560
this.keys = options.keys;
5661
this.method = options.method;
5762
this.settings = options.settings;
@@ -61,6 +66,20 @@ export class ProcessRun {
6166
this.events = options.events;
6267
}
6368

69+
private buildProxyMeta(
70+
proxyType: CheckResult["proxyType"] | undefined,
71+
overrides?: { checkMode?: CheckResult["checkMode"]; targetUrl?: CheckResult["targetUrl"] }
72+
) {
73+
if (!this.isProxyService) {
74+
return { proxyType };
75+
}
76+
return {
77+
proxyType,
78+
checkMode: overrides?.checkMode ?? this.settings.proxy?.checkMode,
79+
targetUrl: overrides?.targetUrl ?? this.settings.proxy?.targetUrl
80+
};
81+
}
82+
6483
start() {
6584
this.status = "Running";
6685
this.emitProgress();
@@ -151,7 +170,9 @@ export class ProcessRun {
151170
this.abortControllers.set(keyIndex, controller);
152171
this.activeCount += 1;
153172

154-
const maskedKey = maskKey(key);
173+
const proxyInfo = decodeProxyKey(key);
174+
const displayKey = proxyInfo?.proxy ?? key;
175+
const maskedKey = this.isProxyService ? displayKey : maskKey(displayKey);
155176
let result: CheckResult;
156177

157178
try {
@@ -162,14 +183,15 @@ export class ProcessRun {
162183
await sleep(delayMs);
163184
}
164185

165-
result = await this.runWithRetries(key, controller.signal);
186+
result = await this.runWithRetries(key, controller.signal, proxyInfo ?? undefined);
166187
} catch (error) {
167188
const message = error instanceof Error ? error.message : "Unexpected error";
168189
result = {
169190
status: "UNKNOWN_ERROR",
170191
latencyMs: 0,
171192
errorMessage: sanitizeErrorMessage(message),
172-
checkedAt: new Date().toISOString()
193+
checkedAt: new Date().toISOString(),
194+
...this.buildProxyMeta(proxyInfo?.proxyType)
173195
};
174196
} finally {
175197
this.activeCount -= 1;
@@ -180,7 +202,7 @@ export class ProcessRun {
180202
this.events.onLog({
181203
processId: this.id,
182204
keyIndex,
183-
keyFull: key,
205+
keyFull: displayKey,
184206
keyMasked: maskedKey,
185207
method: this.method,
186208
result
@@ -190,13 +212,18 @@ export class ProcessRun {
190212
this.schedule();
191213
}
192214

193-
private async runWithRetries(key: string, signal: AbortSignal): Promise<CheckResult> {
215+
private async runWithRetries(
216+
key: string,
217+
signal: AbortSignal,
218+
proxyInfo?: { proxyType: CheckResult["proxyType"] }
219+
): Promise<CheckResult> {
194220
let attempt = 0;
195221
let lastResult: CheckResult = {
196222
status: "UNKNOWN_ERROR",
197223
latencyMs: 0,
198224
checkedAt: new Date().toISOString(),
199-
errorMessage: "No response"
225+
errorMessage: "No response",
226+
...this.buildProxyMeta(proxyInfo?.proxyType)
200227
};
201228

202229
while (attempt <= this.settings.retries) {
@@ -206,7 +233,8 @@ export class ProcessRun {
206233
latencyMs: 0,
207234
checkedAt: new Date().toISOString(),
208235
errorCode: "cancelled",
209-
errorMessage: "Cancelled by user"
236+
errorMessage: "Cancelled by user",
237+
...this.buildProxyMeta(proxyInfo?.proxyType)
210238
};
211239
}
212240

@@ -217,7 +245,8 @@ export class ProcessRun {
217245
timeoutMs: this.settings.timeoutMs,
218246
signal,
219247
customConfig: this.customConfig,
220-
openAiOrgId: this.settings.openAiOrgId
248+
openAiOrgId: this.settings.openAiOrgId,
249+
proxySettings: this.settings.proxy
221250
});
222251

223252
if (this.stopped || signal.aborted) {
@@ -226,7 +255,8 @@ export class ProcessRun {
226255
latencyMs: 0,
227256
checkedAt: new Date().toISOString(),
228257
errorCode: "cancelled",
229-
errorMessage: "Cancelled by user"
258+
errorMessage: "Cancelled by user",
259+
...this.buildProxyMeta(proxyInfo?.proxyType)
230260
};
231261
}
232262

@@ -236,7 +266,11 @@ export class ProcessRun {
236266
latencyMs: adapterResult.latencyMs,
237267
errorCode: adapterResult.errorCode,
238268
errorMessage: adapterResult.errorMessage,
239-
checkedAt: new Date().toISOString()
269+
checkedAt: new Date().toISOString(),
270+
...this.buildProxyMeta(adapterResult.proxyType ?? proxyInfo?.proxyType, {
271+
checkMode: adapterResult.checkMode,
272+
targetUrl: adapterResult.targetUrl
273+
})
240274
};
241275

242276
if (!adapterResult.retryable || attempt === this.settings.retries) {

src/main/main.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { ExportFormat, ReportPayload } from "../shared/types";
99

1010
let mainWindow: BrowserWindow | null = null;
1111
let processManager: ProcessManager | null = null;
12+
const proxyAggregatorControllers = new Map<string, AbortController>();
1213

1314
function createWindow() {
1415
mainWindow = new BrowserWindow({
@@ -52,6 +53,44 @@ function requireManager() {
5253
return processManager;
5354
}
5455

56+
async function fetchTextWithTimeout(
57+
url: string,
58+
timeoutMs: number,
59+
signal?: AbortSignal
60+
) {
61+
const controller = new AbortController();
62+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
63+
const onAbort = () => controller.abort();
64+
if (signal) {
65+
if (signal.aborted) {
66+
controller.abort();
67+
} else {
68+
signal.addEventListener("abort", onAbort, { once: true });
69+
}
70+
}
71+
try {
72+
const response = await fetch(url, {
73+
signal: controller.signal,
74+
headers: {
75+
"User-Agent": "API Key Health Checker"
76+
}
77+
});
78+
if (!response.ok) {
79+
return { error: `HTTP ${response.status}` };
80+
}
81+
const text = await response.text();
82+
return { text };
83+
} catch (error) {
84+
const message = error instanceof Error ? error.message : "Failed to fetch";
85+
return { error: sanitizeErrorMessage(message) };
86+
} finally {
87+
clearTimeout(timeoutId);
88+
if (signal) {
89+
signal.removeEventListener("abort", onAbort);
90+
}
91+
}
92+
}
93+
5594
function registerIpcHandlers() {
5695
ipcMain.handle("get-app-version", () => app.getVersion());
5796

@@ -125,6 +164,57 @@ function registerIpcHandlers() {
125164
);
126165

127166
ipcMain.handle("list-history", () => requireManager().listHistory());
167+
168+
ipcMain.handle(
169+
"fetch-proxy-aggregators",
170+
async (
171+
_event,
172+
payload: { urls: string[]; timeoutMs?: number; requestId?: string }
173+
) => {
174+
const timeoutMs = Math.max(1000, payload.timeoutMs ?? 10000);
175+
const requestId = payload.requestId ?? "";
176+
const controller = new AbortController();
177+
if (requestId) {
178+
proxyAggregatorControllers.set(requestId, controller);
179+
}
180+
const results = await Promise.all(
181+
(payload.urls || []).map(async (url) => {
182+
if (controller.signal.aborted) {
183+
return { url, error: "Cancelled" };
184+
}
185+
try {
186+
new URL(url);
187+
} catch {
188+
return { url, error: "Invalid URL" };
189+
}
190+
const { text, error } = await fetchTextWithTimeout(
191+
url,
192+
timeoutMs,
193+
controller.signal
194+
);
195+
if (controller.signal.aborted) {
196+
return { url, error: "Cancelled" };
197+
}
198+
if (error) {
199+
return { url, error };
200+
}
201+
return { url, content: text ?? "" };
202+
})
203+
);
204+
205+
if (requestId) {
206+
proxyAggregatorControllers.delete(requestId);
207+
}
208+
return { results, cancelled: controller.signal.aborted };
209+
}
210+
);
211+
212+
ipcMain.handle("cancel-proxy-aggregators", (_event, payload: { requestId: string }) => {
213+
const controller = proxyAggregatorControllers.get(payload.requestId);
214+
if (controller) {
215+
controller.abort();
216+
}
217+
});
128218
}
129219

130220
app.whenReady().then(() => {

src/main/preload.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ import type { ExportFormat, ReportPayload } from "../shared/types";
44
contextBridge.exposeInMainWorld("api", {
55
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
66
openKeyFile: (payload: { encoding: string }) => ipcRenderer.invoke("open-key-file", payload),
7+
fetchProxyAggregators: (payload: {
8+
urls: string[];
9+
timeoutMs?: number;
10+
requestId?: string;
11+
}) => ipcRenderer.invoke("fetch-proxy-aggregators", payload),
12+
cancelProxyAggregators: (payload: { requestId: string }) =>
13+
ipcRenderer.invoke("cancel-proxy-aggregators", payload),
714
startCheck: (payload: unknown) => ipcRenderer.invoke("start-check", payload),
815
pauseProcess: (processId: string) => ipcRenderer.invoke("pause-check", processId),
916
resumeProcess: (processId: string) => ipcRenderer.invoke("resume-check", processId),

src/main/report.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ function buildCsv(payload: ReportPayload, includeFull: boolean) {
2020
"latency_ms",
2121
"error_code",
2222
"error_message",
23-
"checked_at"
23+
"checked_at",
24+
"proxy_type",
25+
"check_mode",
26+
"target_url"
2427
].filter(Boolean) as string[];
2528

2629
const lines = [headers.join(",")];
@@ -36,7 +39,10 @@ function buildCsv(payload: ReportPayload, includeFull: boolean) {
3639
String(item.latencyMs ?? 0),
3740
item.errorCode ?? "",
3841
sanitizeErrorMessage(item.errorMessage ?? ""),
39-
item.checkedAt
42+
item.checkedAt,
43+
item.proxyType ?? "",
44+
item.checkMode ?? "",
45+
item.targetUrl ?? ""
4046
);
4147
lines.push(row.map(csvEscape).join(","));
4248
}
@@ -53,7 +59,10 @@ function buildJson(payload: ReportPayload, includeFull: boolean) {
5359
latency_ms: item.latencyMs,
5460
error_code: item.errorCode ?? null,
5561
error_message: sanitizeErrorMessage(item.errorMessage ?? ""),
56-
checked_at: item.checkedAt
62+
checked_at: item.checkedAt,
63+
proxy_type: item.proxyType ?? null,
64+
check_mode: item.checkMode ?? null,
65+
target_url: item.targetUrl ?? null
5766
};
5867

5968
if (includeFull) {

src/main/services/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,15 @@ import { openAiAdapter } from "./openai";
33
import { geminiAdapter } from "./gemini";
44
import { youtubeAdapter } from "./youtube";
55
import { customAdapter } from "./custom";
6+
import { proxyAdapter } from "./proxy";
67

7-
const adapters: ServiceAdapter[] = [openAiAdapter, geminiAdapter, youtubeAdapter, customAdapter];
8+
const adapters: ServiceAdapter[] = [
9+
openAiAdapter,
10+
geminiAdapter,
11+
youtubeAdapter,
12+
proxyAdapter,
13+
customAdapter
14+
];
815

916
export function getServiceAdapter(id: ServiceAdapter["id"]) {
1017
return adapters.find((adapter) => adapter.id === id);

0 commit comments

Comments
 (0)