mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 17:54:47 +00:00
fix(auth): preserve locked profile upsert semantics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user