fix: redact channel status json errors

This commit is contained in:
Gustavo Madeira Santana
2026-04-20 23:06:17 -04:00
parent 89d8986e5d
commit 6e8f0f8d2a
2 changed files with 51 additions and 2 deletions

View File

@@ -251,7 +251,15 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
});
it("keeps JSON fallback structured without rendering config-only text", async () => {
mocks.callGateway.mockRejectedValue(new Error("gateway closed"));
mocks.callGateway.mockRejectedValue(
new Error(
[
"gateway timeout after 3000ms",
"Gateway target: wss://user:pass@gateway.example.com/socket?token=secret-token&keep=visible",
"Source: env OPENCLAW_GATEWAY_URL",
].join("\n"),
),
);
mocks.requireValidConfigSnapshot.mockResolvedValue({ secretResolved: false, channels: {} });
mocks.resolveCommandConfigWithSecrets.mockResolvedValue({
resolvedConfig: { secretResolved: true, channels: {} },
@@ -270,6 +278,9 @@ describe("channelsStatusCommand SecretRef fallback flow", () => {
}),
);
const payload = JSON.parse(logs.at(-1) ?? "{}");
expect(payload.error).toContain("Gateway target:");
expect(payload.error).not.toContain("user:pass");
expect(payload.error).not.toContain("secret-token");
expect(payload).toEqual(
expect.objectContaining({
gatewayReachable: false,

View File

@@ -5,6 +5,7 @@ import { withProgress } from "../../cli/progress.js";
import { readConfigFileSnapshot } from "../../config/config.js";
import { callGateway } from "../../gateway/call.js";
import { collectChannelStatusIssues } from "../../infra/channels-status-issues.js";
import { formatErrorMessage } from "../../infra/errors.js";
import { formatTimeAgo } from "../../infra/format-time/format-relative.ts";
import { listConfiguredChannelIdsForReadOnlyScope } from "../../plugins/channel-plugin-ids.js";
import { defaultRuntime, type RuntimeEnv, writeRuntimeJson } from "../../runtime.js";
@@ -29,6 +30,43 @@ export type ChannelsStatusOptions = {
timeout?: string;
};
const SENSITIVE_GATEWAY_URL_QUERY_KEYS = new Set([
"access_token",
"api_key",
"auth",
"key",
"password",
"secret",
"signature",
"token",
]);
function redactGatewayUrlSecretsInText(text: string): string {
return text.replace(/\b(?:wss?|https?):\/\/[^\s"'<>]+/gi, (rawUrl) => {
try {
const url = new URL(rawUrl);
if (url.username) {
url.username = "redacted";
}
if (url.password) {
url.password = "redacted";
}
for (const key of url.searchParams.keys()) {
if (SENSITIVE_GATEWAY_URL_QUERY_KEYS.has(key.toLowerCase())) {
url.searchParams.set(key, "redacted");
}
}
return url.toString();
} catch {
return rawUrl;
}
});
}
function formatChannelsStatusJsonError(err: unknown): string {
return redactGatewayUrlSecretsInText(formatErrorMessage(err));
}
export function formatGatewayChannelsStatusLines(payload: Record<string, unknown>): string[] {
const lines: string[] = [];
lines.push(theme.success("Gateway reachable."));
@@ -190,7 +228,7 @@ export async function channelsStatusCommand(
if (opts.json) {
writeRuntimeJson(runtime, {
gatewayReachable: false,
error: String(err),
error: formatChannelsStatusJsonError(err),
configOnly: true,
config: {
path: snapshot.path,