From b6631cc29cdcff0fc90336e41acdf4d155dfb7f4 Mon Sep 17 00:00:00 2001 From: "R.D." Date: Wed, 15 Apr 2026 05:40:01 -0400 Subject: [PATCH] fix(healthcheck): unwrap resolved proxy for token refresh --- src/lib/tokenHealthCheck.ts | 18 +- tests/unit/token-health-check.test.mjs | 236 +++++++++++++++++++++++++ 2 files changed, 252 insertions(+), 2 deletions(-) diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index 8f3d94410960..b2cfc3864af5 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -35,6 +35,19 @@ function getConnectionLogLabel(conn: { name?: string; email?: string; id?: strin return pickMaskedDisplayValue([conn.name, conn.email], conn.id || "-"); } +export function extractResolvedProxyConfig(resolvedProxy: unknown) { + if ( + resolvedProxy && + typeof resolvedProxy === "object" && + !Array.isArray(resolvedProxy) && + "proxy" in resolvedProxy + ) { + return (resolvedProxy as { proxy?: unknown }).proxy ?? null; + } + + return resolvedProxy ?? null; +} + export function buildRefreshFailureUpdate(conn: any, now: string) { const wasExpired = conn.testStatus === "expired"; const retryCount = (conn.expiredRetryCount ?? 0) + (wasExpired ? 1 : 0); @@ -202,7 +215,7 @@ async function sweep() { /** * Check a single connection and refresh if due. */ -async function checkConnection(conn) { +export async function checkConnection(conn) { // Determine interval (0 = disabled) const intervalMin = conn.healthCheckInterval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MIN; if (intervalMin <= 0) return; @@ -255,7 +268,8 @@ async function checkConnection(conn) { }; const hideLogs = await shouldHideLogs(); - const proxyConfig = await resolveProxyForConnection(conn.id); + const proxyResolution = await resolveProxyForConnection(conn.id); + const proxyConfig = extractResolvedProxyConfig(proxyResolution); const result = await getAccessToken( conn.provider, credentials, diff --git a/tests/unit/token-health-check.test.mjs b/tests/unit/token-health-check.test.mjs index 6645ac767c78..424020349f38 100644 --- a/tests/unit/token-health-check.test.mjs +++ b/tests/unit/token-health-check.test.mjs @@ -1,10 +1,169 @@ import test from "node:test"; import assert from "node:assert/strict"; +import fs from "node:fs"; +import http from "node:http"; +import net from "node:net"; +import os from "node:os"; +import path from "node:path"; process.env.NODE_ENV = "test"; +const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-token-healthcheck-")); +process.env.DATA_DIR = TEST_DATA_DIR; + +const core = await import("../../src/lib/db/core.ts"); +const providersDb = await import("../../src/lib/db/providers.ts"); +const settingsDb = await import("../../src/lib/db/settings.ts"); +const { PROVIDERS } = await import("../../open-sse/config/constants.ts"); const tokenHealthCheck = await import("../../src/lib/tokenHealthCheck.ts"); +async function resetStorage() { + core.resetDbInstance(); + + for (let attempt = 0; attempt < 10; attempt++) { + try { + if (fs.existsSync(TEST_DATA_DIR)) { + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + } + break; + } catch (error) { + if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) { + await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1))); + } else { + throw error; + } + } + } + + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +async function withHttpServer(handler, fn) { + const server = http.createServer(handler); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + + const address = server.address(); + assert.ok(address && typeof address === "object"); + + try { + return await fn({ + host: "127.0.0.1", + port: address.port, + url: `http://127.0.0.1:${address.port}`, + }); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }); + } +} + +async function withConnectProxyServer(fn) { + const server = http.createServer((_req, res) => { + res.writeHead(501); + res.end("CONNECT only"); + }); + + server.on("connect", (req, clientSocket, head) => { + const [host, portText] = String(req.url || "").split(":"); + const targetPort = Number(portText || 80); + const upstreamSocket = net.connect(targetPort, host, () => { + clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n"); + if (head && head.length > 0) { + upstreamSocket.write(head); + } + upstreamSocket.pipe(clientSocket); + clientSocket.pipe(upstreamSocket); + }); + + const closeSockets = () => { + upstreamSocket.destroy(); + clientSocket.destroy(); + }; + + upstreamSocket.on("error", closeSockets); + clientSocket.on("error", closeSockets); + }); + + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + + const address = server.address(); + assert.ok(address && typeof address === "object"); + + try { + return await fn({ + host: "127.0.0.1", + port: address.port, + url: `http://127.0.0.1:${address.port}`, + }); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) reject(error); + else resolve(); + }); + }); + } +} + +async function withPatchedProvider(providerId, config, fn) { + const hadOwnConfig = Object.prototype.hasOwnProperty.call(PROVIDERS, providerId); + const previousConfig = hadOwnConfig ? PROVIDERS[providerId] : undefined; + PROVIDERS[providerId] = config; + + try { + return await fn(); + } finally { + if (hadOwnConfig) { + PROVIDERS[providerId] = previousConfig; + } else { + delete PROVIDERS[providerId]; + } + } +} + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("extractResolvedProxyConfig unwraps proxy resolution metadata", () => { + const proxy = { + type: "http", + host: "proxy.example.test", + port: 8080, + }; + + assert.deepEqual( + tokenHealthCheck.extractResolvedProxyConfig({ + proxy, + level: "account", + levelId: "conn-123", + source: "registry", + }), + proxy + ); + assert.deepEqual(tokenHealthCheck.extractResolvedProxyConfig(proxy), proxy); + assert.equal( + tokenHealthCheck.extractResolvedProxyConfig({ + proxy: null, + level: "direct", + }), + null + ); + assert.equal(tokenHealthCheck.extractResolvedProxyConfig(null), null); +}); + test("buildRefreshFailureUpdate keeps active connections routable after refresh failure", () => { const now = "2026-04-09T04:40:00.000Z"; @@ -41,3 +200,80 @@ test("buildRefreshFailureUpdate preserves expired retry tracking", () => { assert.equal(update.expiredRetryCount, 3); assert.equal(update.expiredRetryAt, now); }); + +test("checkConnection uses the resolved proxy payload when refreshing tokens", async () => { + await resetStorage(); + + const providerId = "custom-oauth-healthcheck"; + const refreshRequests = []; + + await withHttpServer( + (req, res) => { + let body = ""; + req.setEncoding("utf8"); + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + refreshRequests.push({ + method: req.method, + url: req.url, + headers: req.headers, + body, + }); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + access_token: "new-access-token", + refresh_token: "new-refresh-token", + expires_in: 3600, + }) + ); + }); + }, + async (tokenServer) => { + await withConnectProxyServer(async (proxy) => { + await withPatchedProvider( + providerId, + { + tokenUrl: `${tokenServer.url}/token`, + clientId: "healthcheck-client-id", + clientSecret: "healthcheck-client-secret", + }, + async () => { + const connection = await providersDb.createProviderConnection({ + provider: providerId, + authType: "oauth", + name: "Healthcheck Proxy Account", + email: "healthcheck@example.com", + accessToken: "stale-access-token", + refreshToken: "refresh-token-123", + isActive: true, + }); + + await settingsDb.setProxyForLevel("key", connection.id, { + type: "http", + host: proxy.host, + port: proxy.port, + }); + + await tokenHealthCheck.checkConnection(connection); + + const updated = await providersDb.getProviderConnectionById(connection.id); + + assert.equal(refreshRequests.length, 1); + assert.equal(refreshRequests[0].method, "POST"); + assert.equal(refreshRequests[0].url, "/token"); + assert.match(refreshRequests[0].body, /grant_type=refresh_token/); + assert.match(refreshRequests[0].body, /refresh_token=refresh-token-123/); + assert.equal(updated?.accessToken, "new-access-token"); + assert.equal(updated?.refreshToken, "new-refresh-token"); + assert.equal(updated?.testStatus, "active"); + assert.equal(updated?.lastError ?? null, null); + assert.ok(updated?.tokenExpiresAt); + } + ); + }); + } + ); +});