fix(auth): preserve locked profile upsert semantics

This commit is contained in:
Vincent Koc
2026-05-16 20:51:36 +08:00
parent 605a2c87ae
commit b0daf992b2
5 changed files with 127 additions and 19 deletions

View File

@@ -460,7 +460,11 @@ function createFallbackOAuthProfileSecretKeyFile(): string | undefined {
}
function shouldUseMacKeychainForOAuthProfileSecrets(): boolean {
return process.platform === "darwin" && process.env.VITEST !== "true";
return (
process.platform === "darwin" &&
process.env.VITEST !== "true" &&
process.env.VITEST_WORKER_ID === undefined
);
}
function resolveOAuthProfileSecretKeySeed(options?: { create?: boolean }): string | undefined {

View File

@@ -5,7 +5,7 @@ import { describe, expect, it, vi } from "vitest";
import { resolveOAuthDir } from "../../config/paths.js";
import { AUTH_STORE_VERSION } from "./constants.js";
import { resolveAuthStorePath } from "./paths.js";
import { promoteAuthProfileInOrder } from "./profiles.js";
import { promoteAuthProfileInOrder, upsertAuthProfileWithLock } from "./profiles.js";
import {
clearRuntimeAuthProfileStoreSnapshots,
findPersistedAuthProfileCredential,
@@ -138,6 +138,54 @@ function expectOpenClawCredentialsOAuthRef(
}
describe("promoteAuthProfileInOrder", () => {
it("normalizes copied secrets when using the locked upsert path", async () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-profile-upsert-"));
const agentDir = path.join(stateDir, "agents", "main", "agent");
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = stateDir;
try {
fs.mkdirSync(agentDir, { recursive: true });
await upsertAuthProfileWithLock({
profileId: "openai:manual",
credential: {
type: "token",
provider: "openai",
token: " bearer\r\n-token\u2502 ",
},
agentDir,
});
await upsertAuthProfileWithLock({
profileId: "anthropic:key",
credential: {
type: "api_key",
provider: "anthropic",
key: " sk-\r\nant\u2502 ",
},
agentDir,
});
const profiles = loadAuthProfileStoreWithoutExternalProfiles(agentDir).profiles;
expect(profiles["openai:manual"]).toMatchObject({
type: "token",
provider: "openai",
token: "bearer-token",
});
expect(profiles["anthropic:key"]).toMatchObject({
type: "api_key",
provider: "anthropic",
key: "sk-ant",
});
} finally {
if (previousStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = previousStateDir;
}
fs.rmSync(stateDir, { recursive: true, force: true });
}
});
it("omits inline openai-codex oauth secrets from persisted auth profile files", () => {
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-profile-metadata-"));
const agentDir = path.join(stateDir, "agents", "main", "agent");
@@ -610,10 +658,18 @@ describe("promoteAuthProfileInOrder", () => {
{ filterExternalAuthProfiles: false },
);
const expectedKeyPath =
process.platform === "darwin"
? path.join(
homeDir,
"Library",
"Application Support",
"OpenClaw",
"auth-profile-secret-key",
)
: path.join(homeDir, ".openclaw-auth-profile-secrets", "auth-profile-secret-key");
const keyPaths = findFilesNamed(rootDir, "auth-profile-secret-key");
expect(keyPaths).toEqual([
path.join(homeDir, ".openclaw-auth-profile-secrets", "auth-profile-secret-key"),
]);
expect(keyPaths).toEqual([expectedKeyPath]);
expect(keyPaths.every((keyPath) => !isPathInsideOrEqual(stateDir, keyPath))).toBe(true);
const keyValues = keyPaths.map((keyPath) => fs.readFileSync(keyPath, "utf8").trim());
const persistedStateTree = readPersistedTree(stateDir);

View File

@@ -106,22 +106,35 @@ export async function promoteAuthProfileInOrder(params: {
});
}
function normalizeAuthProfileCredential(credential: AuthProfileCredential): AuthProfileCredential {
if (credential.type === "api_key") {
if (typeof credential.key !== "string") {
return credential;
}
const { key: _key, ...rest } = credential;
const key = normalizeSecretInput(credential.key);
return {
...rest,
...(key ? { key } : {}),
};
}
if (credential.type === "token") {
if (typeof credential.token !== "string") {
return credential;
}
const { token: _token, ...rest } = credential;
const token = normalizeSecretInput(credential.token);
return { ...rest, ...(token ? { token } : {}) };
}
return credential;
}
export function upsertAuthProfile(params: {
profileId: string;
credential: AuthProfileCredential;
agentDir?: string;
}): void {
const credential =
params.credential.type === "api_key"
? {
...params.credential,
...(typeof params.credential.key === "string"
? { key: normalizeSecretInput(params.credential.key) }
: {}),
}
: params.credential.type === "token"
? { ...params.credential, token: normalizeSecretInput(params.credential.token) }
: params.credential;
const credential = normalizeAuthProfileCredential(params.credential);
const store = ensureAuthProfileStoreForLocalUpdate(params.agentDir);
store.profiles[params.profileId] = credential;
saveAuthProfileStore(store, params.agentDir, {
@@ -135,10 +148,15 @@ export async function upsertAuthProfileWithLock(params: {
credential: AuthProfileCredential;
agentDir?: string;
}): Promise<AuthProfileStore | null> {
const credential = normalizeAuthProfileCredential(params.credential);
return await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
saveOptions: {
filterExternalAuthProfiles: false,
syncExternalCli: false,
},
updater: (store) => {
store.profiles[params.profileId] = params.credential;
store.profiles[params.profileId] = credential;
return true;
},
});

View File

@@ -456,6 +456,7 @@ function buildLocalAuthProfileStoreForSave(params: {
export async function updateAuthProfileStoreWithLock(params: {
agentDir?: string;
saveOptions?: SaveAuthProfileStoreOptions;
updater: (store: AuthProfileStore) => boolean;
}): Promise<AuthProfileStore | null> {
const authPath = resolveAuthStorePath(params.agentDir);
@@ -469,7 +470,7 @@ export async function updateAuthProfileStoreWithLock(params: {
const store = loadAuthProfileStoreForAgent(params.agentDir, { syncExternalCli: false });
const shouldSave = params.updater(store);
if (shouldSave) {
saveAuthProfileStore(store, params.agentDir);
saveAuthProfileStore(store, params.agentDir, params.saveOptions);
}
return store;
});

View File

@@ -1,7 +1,31 @@
import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
import { updateAuthProfileStoreWithLock } from "./store.js";
import type { AuthProfileCredential, AuthProfileStore } from "./types.js";
function normalizeAuthProfileCredential(credential: AuthProfileCredential): AuthProfileCredential {
if (credential.type === "api_key") {
if (typeof credential.key !== "string") {
return credential;
}
const { key: _key, ...rest } = credential;
const key = normalizeSecretInput(credential.key);
return {
...rest,
...(key ? { key } : {}),
};
}
if (credential.type === "token") {
if (typeof credential.token !== "string") {
return credential;
}
const { token: _token, ...rest } = credential;
const token = normalizeSecretInput(credential.token);
return { ...rest, ...(token ? { token } : {}) };
}
return credential;
}
export async function upsertAuthProfileWithLock(params: {
profileId: string;
credential: AuthProfileCredential;
@@ -11,10 +35,15 @@ export async function upsertAuthProfileWithLock(params: {
ensureAuthStoreFile(authPath);
try {
const credential = normalizeAuthProfileCredential(params.credential);
return await updateAuthProfileStoreWithLock({
agentDir: params.agentDir,
saveOptions: {
filterExternalAuthProfiles: false,
syncExternalCli: false,
},
updater: (store) => {
store.profiles[params.profileId] = params.credential;
store.profiles[params.profileId] = credential;
return true;
},
});