mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 17:14:45 +00:00
fix(agents): redact oauth refresh errors
This commit is contained in:
@@ -153,6 +153,40 @@ describe("OAuthManagerRefreshError", () => {
|
||||
expect(serialized).not.toContain("store-access");
|
||||
expect(serialized).not.toContain("store-refresh");
|
||||
});
|
||||
|
||||
it("redacts credential secrets from the refresh error message", () => {
|
||||
const refreshedStore: AuthProfileStore = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"openai-codex:default": createCredential({
|
||||
access: "store-access",
|
||||
refresh: "store-refresh",
|
||||
idToken: "store-id-token",
|
||||
}),
|
||||
},
|
||||
};
|
||||
const error = new OAuthManagerRefreshError({
|
||||
credential: createCredential({
|
||||
access: "error-access",
|
||||
refresh: "error-refresh",
|
||||
idToken: "error-id-token",
|
||||
}),
|
||||
profileId: "openai-codex:default",
|
||||
refreshedStore,
|
||||
cause: new Error(
|
||||
"refresh rejected error-access error-refresh error-id-token store-access store-refresh store-id-token",
|
||||
),
|
||||
});
|
||||
|
||||
expect(error.message).toContain("refresh rejected");
|
||||
expect(error.message).not.toContain("error-access");
|
||||
expect(error.message).not.toContain("error-refresh");
|
||||
expect(error.message).not.toContain("error-id-token");
|
||||
expect(error.message).not.toContain("store-access");
|
||||
expect(error.message).not.toContain("store-refresh");
|
||||
expect(error.message).not.toContain("store-id-token");
|
||||
expect(error.message.match(/\[redacted\]/g)?.length).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createOAuthManager", () => {
|
||||
|
||||
@@ -80,10 +80,17 @@ export class OAuthManagerRefreshError extends Error {
|
||||
structuredCause?.code === "refresh_contention" && structuredCause.cause
|
||||
? structuredCause.cause
|
||||
: params.cause;
|
||||
super(
|
||||
`OAuth token refresh failed for ${params.credential.provider}: ${formatErrorMessage(params.cause)}`,
|
||||
{ cause: delegatedCause },
|
||||
const storedCredential = params.refreshedStore.profiles[params.profileId];
|
||||
const causeMessage = redactOAuthCredentialSecrets(
|
||||
formatErrorMessage(params.cause),
|
||||
collectOAuthCredentialSecrets(
|
||||
params.credential,
|
||||
storedCredential?.type === "oauth" ? storedCredential : undefined,
|
||||
),
|
||||
);
|
||||
super(`OAuth token refresh failed for ${params.credential.provider}: ${causeMessage}`, {
|
||||
cause: delegatedCause,
|
||||
});
|
||||
this.name = "OAuthManagerRefreshError";
|
||||
this.#credential = params.credential;
|
||||
this.profileId = params.profileId;
|
||||
@@ -154,6 +161,28 @@ function canReuseOAuthCredentialAfterRefreshFailure(params: {
|
||||
return !params.forceRefresh || hasOAuthCredentialChanged(params.attempted, params.candidate);
|
||||
}
|
||||
|
||||
function collectOAuthCredentialSecrets(
|
||||
...credentials: Array<OAuthCredential | undefined>
|
||||
): string[] {
|
||||
const secrets = new Set<string>();
|
||||
for (const credential of credentials) {
|
||||
for (const secret of [credential?.access, credential?.refresh, credential?.idToken]) {
|
||||
if (secret) {
|
||||
secrets.add(secret);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(secrets);
|
||||
}
|
||||
|
||||
function redactOAuthCredentialSecrets(message: string, secrets: string[]): string {
|
||||
let redacted = message;
|
||||
for (const secret of secrets) {
|
||||
redacted = redacted.split(secret).join("[redacted]");
|
||||
}
|
||||
return redacted;
|
||||
}
|
||||
|
||||
async function loadFreshStoredOAuthCredential(params: {
|
||||
profileId: string;
|
||||
agentDir?: string;
|
||||
|
||||
Reference in New Issue
Block a user