fix(agents): redact oauth refresh errors

This commit is contained in:
Vincent Koc
2026-05-16 22:54:15 +08:00
parent 43c53174c5
commit c818a9fb4e
2 changed files with 66 additions and 3 deletions

View File

@@ -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", () => {

View File

@@ -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;