






















@@ -3,6 +3,7 @@ import os from "node:os";
33import path from "node:path";
44import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
55import type { OpenClawConfig } from "../../config/types.openclaw.js";
6+import { formatErrorMessage } from "../../infra/errors.js";
67import { captureEnv } from "../../test-utils/env.js";
78import { __testing as externalAuthTesting } from "./external-auth.js";
89import {
@@ -186,6 +187,72 @@ describe("OAuthManagerRefreshError", () => {
186187expect(error.message).not.toContain("store-refresh");
187188expect(error.message).not.toContain("store-id-token");
188189expect(error.message.match(/\[redacted\]/g)?.length).toBe(6);
190+const surfacedCauseMessage = formatErrorMessage(error.cause);
191+expect(surfacedCauseMessage).not.toContain("error-access");
192+expect(surfacedCauseMessage).not.toContain("error-refresh");
193+expect(surfacedCauseMessage).not.toContain("error-id-token");
194+expect(surfacedCauseMessage).not.toContain("store-access");
195+expect(surfacedCauseMessage).not.toContain("store-refresh");
196+expect(surfacedCauseMessage).not.toContain("store-id-token");
197+expect(surfacedCauseMessage.match(/\[redacted\]/g)?.length).toBe(6);
198+});
199+200+it("redacts token-shaped credential secrets before generic masking", () => {
201+const access = "sk-oauthreviewredaction1234567890zzzz";
202+const refresh = "ya29.oauthreviewredaction1234567890yyyy";
203+const error = new OAuthManagerRefreshError({
204+credential: createCredential({ access, refresh }),
205+profileId: "openai-codex:default",
206+refreshedStore: { version: 1, profiles: {} },
207+cause: new Error(`refresh rejected ${access} ${refresh}`, {
208+cause: new Error(`nested failure ${access}`),
209+}),
210+});
211+212+const surfacedCauseMessage = formatErrorMessage(error.cause);
213+for (const message of [error.message, surfacedCauseMessage]) {
214+expect(message).not.toContain(access);
215+expect(message).not.toContain(refresh);
216+expect(message).not.toContain("sk-oau");
217+expect(message).not.toContain("zzzz");
218+expect(message).not.toContain("ya29.o");
219+expect(message).not.toContain("yyyy");
220+expect(message.match(/\[redacted\]/g)?.length).toBe(3);
221+}
222+});
223+224+it.each([undefined, Symbol("refresh-failed"), () => "refresh-failed"])(
225+"formats non-json refresh failure values without throwing",
226+(cause) => {
227+const error = new OAuthManagerRefreshError({
228+credential: createCredential({
229+access: "sk-nonjsonredaction1234567890zzzz",
230+}),
231+profileId: "openai-codex:default",
232+refreshedStore: { version: 1, profiles: {} },
233+ cause,
234+});
235+236+expect(error.message).toContain("OAuth token refresh failed");
237+},
238+);
239+240+it("redacts overlapping credential secrets longest first", () => {
241+const error = new OAuthManagerRefreshError({
242+credential: createCredential({
243+access: "abc123",
244+refresh: "abc123456",
245+}),
246+profileId: "openai-codex:default",
247+refreshedStore: { version: 1, profiles: {} },
248+cause: new Error("refresh rejected abc123 abc123456"),
249+});
250+251+expect(error.message).toContain("refresh rejected");
252+expect(error.message).not.toContain("abc123");
253+expect(error.message).not.toContain("abc123456");
254+expect(error.message).not.toContain("[redacted]456");
255+expect(error.message.match(/\[redacted\]/g)?.length).toBe(2);
189256});
190257});
191258@@ -380,4 +447,70 @@ describe("createOAuthManager", () => {
380447expect(result.credential.access).toBe("rotated-access");
381448expect(result.credential.refresh).toBe("rotated-refresh");
382449});
450+451+it("redacts the external oauth credential attempted during refresh failures", async () => {
452+const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-redact-"));
453+tempDirs.push(tempRoot);
454+process.env.OPENCLAW_STATE_DIR = tempRoot;
455+const agentDir = path.join(tempRoot, "agents", "sub", "agent");
456+await fs.mkdir(agentDir, { recursive: true });
457+const profileId = "minimax-portal:default";
458+const localCredential = createCredential({
459+provider: "minimax-portal",
460+access: "fresh-local-access",
461+refresh: "fresh-local-refresh",
462+expires: Date.now() + 60_000,
463+});
464+const externalCredential = createCredential({
465+provider: "minimax-portal",
466+access: "external-attempt-access",
467+refresh: "external-attempt-refresh",
468+idToken: "external-attempt-id-token",
469+expires: Date.now() - 30_000,
470+});
471+saveAuthProfileStore(
472+{
473+version: 1,
474+profiles: {
475+[profileId]: localCredential,
476+},
477+},
478+agentDir,
479+{ filterExternalAuthProfiles: false },
480+);
481+482+const manager = createOAuthManager({
483+buildApiKey: async (_provider, credential) => credential.access,
484+refreshCredential: vi.fn(async () => {
485+throw new Error(
486+"refresh rejected external-attempt-access external-attempt-refresh external-attempt-id-token",
487+);
488+}),
489+readBootstrapCredential: () => externalCredential,
490+isRefreshTokenReusedError: () => false,
491+});
492+493+try {
494+await manager.resolveOAuthAccess({
495+store: ensureAuthProfileStore(agentDir),
496+ profileId,
497+credential: localCredential,
498+ agentDir,
499+forceRefresh: true,
500+});
501+throw new Error("Expected refresh failure");
502+} catch (caught) {
503+if (!(caught instanceof OAuthManagerRefreshError)) {
504+throw caught;
505+}
506+expect(caught.message).toContain("refresh rejected");
507+expect(caught.message).not.toContain("external-attempt-access");
508+expect(caught.message).not.toContain("external-attempt-refresh");
509+expect(caught.message).not.toContain("external-attempt-id-token");
510+const surfacedCauseMessage = formatErrorMessage(caught.cause);
511+expect(surfacedCauseMessage).not.toContain("external-attempt-access");
512+expect(surfacedCauseMessage).not.toContain("external-attempt-refresh");
513+expect(surfacedCauseMessage).not.toContain("external-attempt-id-token");
514+}
515+});
383516});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。