From b855b1d047eea2c578320e63ee12a956443e6bdd Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 15 Apr 2026 12:08:33 +0100 Subject: [PATCH] fix(ci): clear extension lint regressions --- extensions/qa-channel/src/bus-client.ts | 17 ++- extensions/qa-channel/src/channel.test.ts | 8 +- extensions/qqbot/src/api.ts | 120 +++++++++++++--------- 3 files changed, 87 insertions(+), 58 deletions(-) diff --git a/extensions/qa-channel/src/bus-client.ts b/extensions/qa-channel/src/bus-client.ts index 77bd143d09a..a5cae3ffd1d 100644 --- a/extensions/qa-channel/src/bus-client.ts +++ b/extensions/qa-channel/src/bus-client.ts @@ -1,5 +1,6 @@ import http from "node:http"; import https from "node:https"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import type { QaBusConversation, QaBusEvent, @@ -271,9 +272,17 @@ export async function injectQaBusInboundMessage(params: { } export async function getQaBusState(baseUrl: string): Promise { - const response = await fetch(buildQaBusUrl(baseUrl, "/v1/state")); - if (!response.ok) { - throw new Error(`qa-bus request failed: ${response.status}`); + const { response, release } = await fetchWithSsrFGuard({ + url: buildQaBusUrl(baseUrl, "/v1/state").toString(), + policy: { allowPrivateNetwork: true }, + auditContext: "qa-channel.bus-state", + }); + try { + if (!response.ok) { + throw new Error(`qa-bus request failed: ${response.status}`); + } + return (await response.json()) as QaBusStateSnapshot; + } finally { + await release(); } - return (await response.json()) as QaBusStateSnapshot; } diff --git a/extensions/qa-channel/src/channel.test.ts b/extensions/qa-channel/src/channel.test.ts index 1471b01a8d6..e9c8afad980 100644 --- a/extensions/qa-channel/src/channel.test.ts +++ b/extensions/qa-channel/src/channel.test.ts @@ -1,11 +1,11 @@ import type { PluginRuntime } from "openclaw/plugin-sdk/core"; -import { afterEach, describe, expect, it } from "vitest"; -import { extractToolPayload } from "../../../src/infra/outbound/tool-payload.js"; import { resetPluginRuntimeStateForTest, setActivePluginRegistry, -} from "../../../src/plugins/runtime.js"; -import { createTestRegistry } from "../../../src/test-utils/channel-plugins.js"; +} from "openclaw/plugin-sdk/testing"; +import { afterEach, describe, expect, it } from "vitest"; +import { extractToolPayload } from "../../../src/infra/outbound/tool-payload.js"; +import { createTestRegistry } from "../../../test/helpers/plugins/plugin-registry.js"; import { createStartAccountContext } from "../../../test/helpers/plugins/start-account-context.js"; import { createQaBusState, startQaBusServer } from "../../qa-lab/api.js"; import { qaChannelPlugin, setQaChannelRuntime } from "../api.js"; diff --git a/extensions/qqbot/src/api.ts b/extensions/qqbot/src/api.ts index 511ff6ca637..43e9fd4be1e 100644 --- a/extensions/qqbot/src/api.ts +++ b/extensions/qqbot/src/api.ts @@ -1,6 +1,7 @@ import { createRequire } from "node:module"; import os from "node:os"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; +import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { debugLog, debugError } from "./utils/debug-log.js"; import { sanitizeFileName } from "./utils/platform.js"; @@ -112,12 +113,19 @@ async function doFetchToken(appId: string, clientSecret: string): Promise>> POST ${TOKEN_URL}`); let response: Response; + let release = async () => {}; try { - response = await fetch(TOKEN_URL, { - method: "POST", - headers: requestHeaders, - body: JSON.stringify(requestBody), + const guarded = await fetchWithSsrFGuard({ + url: TOKEN_URL, + init: { + method: "POST", + headers: requestHeaders, + body: JSON.stringify(requestBody), + }, + auditContext: "qqbot.token", }); + response = guarded.response; + release = guarded.release; } catch (err) { debugError(`[qqbot-api:${appId}] <<< Network error:`, err); throw new Error(`Network error getting access_token: ${formatErrorMessage(err)}`, { @@ -125,44 +133,48 @@ async function doFetchToken(appId: string, clientSecret: string): Promise = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - const tokenTraceId = response.headers.get("x-tps-trace-id") ?? ""; - debugLog( - `[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}${tokenTraceId ? ` | TraceId: ${tokenTraceId}` : ""}`, - ); - - let data: { access_token?: string; expires_in?: number }; - let rawBody: string; try { - rawBody = await response.text(); - // Redact the token before logging the raw response body. - const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"'); - debugLog(`[qqbot-api:${appId}] <<< Body:`, logBody); - data = JSON.parse(rawBody) as { access_token?: string; expires_in?: number }; - } catch (err) { - debugError(`[qqbot-api:${appId}] <<< Parse error:`, err); - throw new Error(`Failed to parse access_token response: ${formatErrorMessage(err)}`, { - cause: err, + const responseHeaders: Record = {}; + response.headers.forEach((value, key) => { + responseHeaders[key] = value; }); + const tokenTraceId = response.headers.get("x-tps-trace-id") ?? ""; + debugLog( + `[qqbot-api:${appId}] <<< Status: ${response.status} ${response.statusText}${tokenTraceId ? ` | TraceId: ${tokenTraceId}` : ""}`, + ); + + let data: { access_token?: string; expires_in?: number }; + let rawBody: string; + try { + rawBody = await response.text(); + // Redact the token before logging the raw response body. + const logBody = rawBody.replace(/"access_token"\s*:\s*"[^"]+"/g, '"access_token": "***"'); + debugLog(`[qqbot-api:${appId}] <<< Body:`, logBody); + data = JSON.parse(rawBody) as { access_token?: string; expires_in?: number }; + } catch (err) { + debugError(`[qqbot-api:${appId}] <<< Parse error:`, err); + throw new Error(`Failed to parse access_token response: ${formatErrorMessage(err)}`, { + cause: err, + }); + } + + if (!data.access_token) { + throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`); + } + + const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1000; + + tokenCacheMap.set(appId, { + token: data.access_token, + expiresAt, + appId, + }); + + debugLog(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`); + return data.access_token; + } finally { + await release(); } - - if (!data.access_token) { - throw new Error(`Failed to get access_token: ${JSON.stringify(data)}`); - } - - const expiresAt = Date.now() + (data.expires_in ?? 7200) * 1000; - - tokenCacheMap.set(appId, { - token: data.access_token, - expiresAt, - appId, - }); - - debugLog(`[qqbot-api:${appId}] Token cached, expires at: ${new Date(expiresAt).toISOString()}`); - return data.access_token; } /** Clear one token cache or all token caches. */ @@ -247,8 +259,15 @@ export async function apiRequest( } let res: Response; + let release = async () => {}; try { - res = await fetch(url, options); + const guarded = await fetchWithSsrFGuard({ + url, + init: options, + auditContext: `qqbot.api${path}`, + }); + res = guarded.response; + release = guarded.release; } catch (err) { clearTimeout(timeoutId); if (err instanceof Error && err.name === "AbortError") { @@ -270,24 +289,25 @@ export async function apiRequest( `[qqbot-api] <<< Status: ${res.status} ${res.statusText}${traceId ? ` | TraceId: ${traceId}` : ""}`, ); - let data: T; - let rawBody: string; try { - rawBody = await res.text(); + let data: T; + const rawBody = await res.text(); debugLog(`[qqbot-api] <<< Body:`, rawBody); data = JSON.parse(rawBody) as T; + + if (!res.ok) { + const error = data as { message?: string; code?: number }; + throw new Error(`API Error [${path}]: ${error.message ?? JSON.stringify(data)}`); + } + + return data; } catch (err) { throw new Error(`Failed to parse response[${path}]: ${formatErrorMessage(err)}`, { cause: err, }); + } finally { + await release(); } - - if (!res.ok) { - const error = data as { message?: string; code?: number }; - throw new Error(`API Error [${path}]: ${error.message ?? JSON.stringify(data)}`); - } - - return data; } // Upload retry with exponential backoff.