mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-27 17:55:58 +00:00
fix: scope auth profile keys to env
This commit is contained in:
@@ -9,24 +9,39 @@ const LEGACY_AUTH_PROFILE_FILENAME = "auth-profiles.json";
|
||||
const LEGACY_AUTH_STATE_FILENAME = "auth-state.json";
|
||||
const LEGACY_AUTH_FILENAME = "auth.json";
|
||||
|
||||
export function resolveAuthProfileStoreAgentDir(agentDir?: string): string {
|
||||
return resolveUserPath(agentDir ?? resolveDefaultAgentDir({}));
|
||||
export function resolveAuthProfileStoreAgentDir(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return resolveUserPath(agentDir ?? resolveDefaultAgentDir({}, env), env);
|
||||
}
|
||||
|
||||
export function resolveAuthProfileStoreKey(agentDir?: string): string {
|
||||
return resolveAuthProfileStoreAgentDir(agentDir);
|
||||
export function resolveAuthProfileStoreKey(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return resolveAuthProfileStoreAgentDir(agentDir, env);
|
||||
}
|
||||
|
||||
export function resolveAuthStorePath(agentDir?: string): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir), LEGACY_AUTH_PROFILE_FILENAME);
|
||||
export function resolveAuthStorePath(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir, env), LEGACY_AUTH_PROFILE_FILENAME);
|
||||
}
|
||||
|
||||
export function resolveLegacyAuthStorePath(agentDir?: string): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir), LEGACY_AUTH_FILENAME);
|
||||
export function resolveLegacyAuthStorePath(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir, env), LEGACY_AUTH_FILENAME);
|
||||
}
|
||||
|
||||
export function resolveAuthStatePath(agentDir?: string): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir), LEGACY_AUTH_STATE_FILENAME);
|
||||
export function resolveAuthStatePath(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return path.join(resolveAuthProfileStoreAgentDir(agentDir, env), LEGACY_AUTH_STATE_FILENAME);
|
||||
}
|
||||
|
||||
export function resolveAuthStorePathForDisplay(agentDir?: string): string {
|
||||
@@ -43,7 +58,7 @@ export function resolveAuthProfileStoreLocationForDisplay(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return `${resolveOpenClawStateSqlitePath(env)}#table/auth_profile_stores/${resolveAuthProfileStoreKey(agentDir)}`;
|
||||
return `${resolveOpenClawStateSqlitePath(env)}#table/auth_profile_stores/${resolveAuthProfileStoreKey(agentDir, env)}`;
|
||||
}
|
||||
|
||||
export const OAUTH_REFRESH_LOCK_SCOPE = "auth.oauth-refresh";
|
||||
|
||||
@@ -44,6 +44,20 @@ describe("auth profile path helpers (direct-import coverage attribution)", () =>
|
||||
expect(resolved.endsWith(path.join("agents", "main", "agent"))).toBe(true);
|
||||
});
|
||||
|
||||
it("resolves the default auth profile store key from an env override", async () => {
|
||||
const otherStateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-path-direct-env-"));
|
||||
try {
|
||||
const resolved = resolveAuthProfileStoreKey(undefined, {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: otherStateDir,
|
||||
});
|
||||
expect(resolved.startsWith(otherStateDir)).toBe(true);
|
||||
expect(resolved.endsWith(path.join("agents", "main", "agent"))).toBe(true);
|
||||
} finally {
|
||||
await fs.rm(otherStateDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves the display location as a SQLite table target", () => {
|
||||
const agentDir = path.join(stateDir, "agents", "main", "agent");
|
||||
const resolved = resolveAuthProfileStoreLocationForDisplay(agentDir, {
|
||||
|
||||
@@ -38,8 +38,11 @@ import type {
|
||||
ProfileUsageStats,
|
||||
} from "./types.js";
|
||||
|
||||
export function authProfileStoreKey(agentDir?: string): string {
|
||||
return resolveAuthProfileStoreKey(agentDir);
|
||||
export function authProfileStoreKey(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return resolveAuthProfileStoreKey(agentDir, env);
|
||||
}
|
||||
|
||||
export type PersistedAuthProfileStoreEntry = {
|
||||
@@ -889,11 +892,11 @@ function preserveLegacyOAuthRefsForDoctorMigration(
|
||||
export function loadPersistedAuthProfileStoreEntryFromDatabase(
|
||||
database: OpenClawStateDatabase,
|
||||
agentDir?: string,
|
||||
_options: Pick<OpenClawStateDatabaseOptions, "env"> = {},
|
||||
options: Pick<OpenClawStateDatabaseOptions, "env"> = {},
|
||||
): PersistedAuthProfileStoreEntry | null {
|
||||
const result = readAuthProfileStorePayloadResultFromDatabase(
|
||||
database,
|
||||
authProfileStoreKey(agentDir),
|
||||
authProfileStoreKey(agentDir, options.env),
|
||||
);
|
||||
if (!result.exists || result.value === undefined) {
|
||||
return null;
|
||||
@@ -907,7 +910,7 @@ export function loadPersistedAuthProfileStoreEntryFromDatabase(
|
||||
...store,
|
||||
...mergeAuthProfileState(
|
||||
coerceAuthProfileState(raw),
|
||||
loadPersistedAuthProfileStateFromDatabase(database, agentDir),
|
||||
loadPersistedAuthProfileStateFromDatabase(database, agentDir, options),
|
||||
),
|
||||
};
|
||||
return {
|
||||
@@ -920,7 +923,10 @@ export function loadPersistedAuthProfileStoreEntry(
|
||||
agentDir?: string,
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): PersistedAuthProfileStoreEntry | null {
|
||||
const result = readAuthProfileStorePayloadResult(authProfileStoreKey(agentDir), options);
|
||||
const result = readAuthProfileStorePayloadResult(
|
||||
authProfileStoreKey(agentDir, options.env),
|
||||
options,
|
||||
);
|
||||
if (!result.exists || result.value === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -944,10 +950,10 @@ export function loadPersistedAuthProfileStoreEntry(
|
||||
|
||||
export function loadLegacyAuthProfileStoreEntry(
|
||||
agentDir?: string,
|
||||
_options: OpenClawStateDatabaseOptions = {},
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): PersistedAuthProfileStoreEntry | null {
|
||||
const authPath = path.join(
|
||||
resolveAuthProfileStoreAgentDir(agentDir),
|
||||
resolveAuthProfileStoreAgentDir(agentDir, options.env),
|
||||
LEGACY_AUTH_PROFILE_FILENAME,
|
||||
);
|
||||
const raw = loadJsonFile(authPath);
|
||||
@@ -984,7 +990,7 @@ export function savePersistedAuthProfileSecretsStore(
|
||||
env: options.env,
|
||||
});
|
||||
writeAuthProfileStorePayload(
|
||||
authProfileStoreKey(agentDir),
|
||||
authProfileStoreKey(agentDir, options.env),
|
||||
payload as unknown as AuthProfilePayloadValue,
|
||||
options,
|
||||
);
|
||||
@@ -995,10 +1001,11 @@ export function savePersistedAuthProfileSecretsStoreInTransaction(
|
||||
store: AuthProfileSecretsStore,
|
||||
agentDir?: string,
|
||||
updatedAt: number = Date.now(),
|
||||
options: Pick<OpenClawStateDatabaseOptions, "env"> = {},
|
||||
): void {
|
||||
writeAuthProfileStorePayloadInTransaction(
|
||||
database,
|
||||
authProfileStoreKey(agentDir),
|
||||
authProfileStoreKey(agentDir, options.env),
|
||||
store as unknown as AuthProfilePayloadValue,
|
||||
updatedAt,
|
||||
);
|
||||
@@ -1008,5 +1015,6 @@ export function hasPersistedAuthProfileSecretsStore(
|
||||
agentDir?: string,
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): boolean {
|
||||
return readAuthProfileStorePayloadResult(authProfileStoreKey(agentDir), options).exists;
|
||||
return readAuthProfileStorePayloadResult(authProfileStoreKey(agentDir, options.env), options)
|
||||
.exists;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,101 @@ import {
|
||||
} from "./profiles.js";
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
loadAuthProfileStoreForRuntime,
|
||||
saveAuthProfileStore,
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
describe("promoteAuthProfileInOrder", () => {
|
||||
it("uses env-scoped default agent keys when agentDir is omitted", async () => {
|
||||
const processStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-process-state-"));
|
||||
const envStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-env-state-"));
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
process.env.OPENCLAW_STATE_DIR = processStateDir;
|
||||
const env = { ...process.env, OPENCLAW_STATE_DIR: envStateDir };
|
||||
const profileId = "openai-codex:env-default";
|
||||
try {
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "token",
|
||||
provider: "openai-codex",
|
||||
token: "env-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
undefined,
|
||||
{ env },
|
||||
);
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
|
||||
const loaded = loadAuthProfileStoreWithoutExternalProfiles(undefined, { env });
|
||||
|
||||
expect(loaded.profiles[profileId]).toMatchObject({
|
||||
type: "token",
|
||||
provider: "openai-codex",
|
||||
token: "env-token",
|
||||
});
|
||||
expect(fs.existsSync(path.join(envStateDir, "state", "openclaw.sqlite"))).toBe(true);
|
||||
expect(fs.existsSync(path.join(processStateDir, "state", "openclaw.sqlite"))).toBe(false);
|
||||
} finally {
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
fs.rmSync(processStateDir, { recursive: true, force: true });
|
||||
fs.rmSync(envStateDir, { recursive: true, force: true });
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
}
|
||||
});
|
||||
|
||||
it("reads env-scoped legacy default auth profiles before migration", async () => {
|
||||
const processStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-legacy-process-"));
|
||||
const envStateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-legacy-env-"));
|
||||
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
||||
process.env.OPENCLAW_STATE_DIR = processStateDir;
|
||||
const env = { ...process.env, OPENCLAW_STATE_DIR: envStateDir };
|
||||
const agentDir = path.join(envStateDir, "agents", "main", "agent");
|
||||
const profileId = "openai-codex:legacy-default";
|
||||
try {
|
||||
fs.mkdirSync(agentDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(agentDir, "auth-profiles.json"),
|
||||
`${JSON.stringify({
|
||||
version: AUTH_STORE_VERSION,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "token",
|
||||
provider: "openai-codex",
|
||||
token: "legacy-env-token",
|
||||
},
|
||||
},
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
const loaded = loadAuthProfileStoreWithoutExternalProfiles(undefined, { env });
|
||||
|
||||
expect(loaded.profiles[profileId]).toMatchObject({
|
||||
type: "token",
|
||||
provider: "openai-codex",
|
||||
token: "legacy-env-token",
|
||||
});
|
||||
} finally {
|
||||
if (previousStateDir === undefined) {
|
||||
delete process.env.OPENCLAW_STATE_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
||||
}
|
||||
fs.rmSync(processStateDir, { recursive: true, force: true });
|
||||
fs.rmSync(envStateDir, { recursive: true, force: true });
|
||||
clearRuntimeAuthProfileStoreSnapshots();
|
||||
}
|
||||
});
|
||||
|
||||
it("moves a relogin profile to the front of an existing per-agent provider order", async () => {
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-order-promote-"));
|
||||
const agentDir = path.join(stateDir, "agents", "main", "agent");
|
||||
|
||||
@@ -42,8 +42,11 @@ const AUTH_FAILURE_REASONS = new Set<AuthProfileFailureReason>([
|
||||
const AUTH_BLOCKED_REASONS = new Set<AuthProfileBlockedReason>(["subscription_limit"]);
|
||||
const AUTH_BLOCKED_SOURCES = new Set<AuthProfileBlockedSource>(["codex_rate_limits", "wham"]);
|
||||
|
||||
export function authProfileStateKey(agentDir?: string): string {
|
||||
return resolveAuthProfileStoreKey(agentDir);
|
||||
export function authProfileStateKey(
|
||||
agentDir?: string,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): string {
|
||||
return resolveAuthProfileStoreKey(agentDir, env);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
@@ -208,7 +211,7 @@ export function loadPersistedAuthProfileState(
|
||||
agentDir?: string,
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): AuthProfileState {
|
||||
const key = authProfileStateKey(agentDir);
|
||||
const key = authProfileStateKey(agentDir, options.env);
|
||||
const sqliteState = readAuthProfileStatePayloadResult(key, options);
|
||||
if (sqliteState.exists && sqliteState.value !== undefined) {
|
||||
return coerceAuthProfileState(sqliteState.value);
|
||||
@@ -220,8 +223,9 @@ export function loadPersistedAuthProfileState(
|
||||
export function loadPersistedAuthProfileStateFromDatabase(
|
||||
database: OpenClawStateDatabase,
|
||||
agentDir?: string,
|
||||
options: Pick<OpenClawStateDatabaseOptions, "env"> = {},
|
||||
): AuthProfileState {
|
||||
const key = authProfileStateKey(agentDir);
|
||||
const key = authProfileStateKey(agentDir, options.env);
|
||||
const sqliteState = readAuthProfileStatePayloadResultFromDatabase(database, key);
|
||||
if (sqliteState.exists && sqliteState.value !== undefined) {
|
||||
return coerceAuthProfileState(sqliteState.value);
|
||||
@@ -262,10 +266,11 @@ export function savePersistedAuthProfileStateInTransaction(
|
||||
store: AuthProfileState,
|
||||
agentDir?: string,
|
||||
updatedAt: number = Date.now(),
|
||||
options: Pick<OpenClawStateDatabaseOptions, "env"> = {},
|
||||
): AuthProfileStateStore | null {
|
||||
return savePersistedAuthProfileStatePayload({
|
||||
store,
|
||||
key: authProfileStateKey(agentDir),
|
||||
key: authProfileStateKey(agentDir, options.env),
|
||||
write: (key, payload) =>
|
||||
writeAuthProfileStatePayloadInTransaction(
|
||||
database,
|
||||
|
||||
@@ -346,15 +346,20 @@ function saveAuthProfileStoreInTransaction(
|
||||
const localStore = buildLocalAuthProfileStoreForSave({ store, agentDir, options });
|
||||
const previousRaw = readAuthProfileStorePayloadResultFromDatabase(
|
||||
database,
|
||||
resolveAuthProfileStoreKey(agentDir),
|
||||
resolveAuthProfileStoreKey(agentDir, options?.env),
|
||||
);
|
||||
const payload = buildPersistedAuthProfileSecretsStore(localStore, undefined, {
|
||||
agentDir,
|
||||
env: options?.env,
|
||||
existingRaw: previousRaw.exists ? previousRaw.value : undefined,
|
||||
});
|
||||
savePersistedAuthProfileSecretsStoreInTransaction(database, payload, agentDir);
|
||||
savePersistedAuthProfileStateInTransaction(database, localStore, agentDir);
|
||||
const updatedAt = Date.now();
|
||||
savePersistedAuthProfileSecretsStoreInTransaction(database, payload, agentDir, updatedAt, {
|
||||
env: options?.env,
|
||||
});
|
||||
savePersistedAuthProfileStateInTransaction(database, localStore, agentDir, updatedAt, {
|
||||
env: options?.env,
|
||||
});
|
||||
return localStore;
|
||||
}
|
||||
|
||||
@@ -395,7 +400,7 @@ export async function updateAuthProfileStoreWithLock(params: {
|
||||
);
|
||||
if (savedStore) {
|
||||
writeCachedAuthProfileStore({
|
||||
storeKey: resolveAuthProfileStoreKey(params.agentDir),
|
||||
storeKey: resolveAuthProfileStoreKey(params.agentDir, params.env),
|
||||
authMtimeMs: Date.now(),
|
||||
store: savedStore,
|
||||
});
|
||||
@@ -426,7 +431,7 @@ function loadAuthProfileStoreForAgent(
|
||||
options?: LoadAuthProfileStoreOptions,
|
||||
): AuthProfileStore {
|
||||
const readOnly = options?.readOnly === true;
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir);
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir, options?.env);
|
||||
let persisted = loadPersistedAuthProfileStoreEntry(agentDir, { env: options?.env });
|
||||
let authMtimeMs = persisted?.updatedAt ?? null;
|
||||
if (!persisted) {
|
||||
@@ -482,8 +487,8 @@ export function loadAuthProfileStoreForRuntime(
|
||||
options?: LoadAuthProfileStoreOptions,
|
||||
): AuthProfileStore {
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, options);
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir);
|
||||
const mainStoreKey = resolveAuthProfileStoreKey();
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir, options?.env);
|
||||
const mainStoreKey = resolveAuthProfileStoreKey(undefined, options?.env);
|
||||
const externalCli = resolveExternalCliOverlayOptions(options);
|
||||
if (!agentDir || storeKey === mainStoreKey) {
|
||||
return overlayExternalAuthProfiles(store, {
|
||||
@@ -518,8 +523,8 @@ export function loadAuthProfileStoreWithoutExternalProfiles(
|
||||
...(options?.env ? { env: options.env } : {}),
|
||||
};
|
||||
const store = loadAuthProfileStoreForAgent(agentDir, loadOptions);
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir);
|
||||
const mainStoreKey = resolveAuthProfileStoreKey();
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir, options?.env);
|
||||
const mainStoreKey = resolveAuthProfileStoreKey(undefined, options?.env);
|
||||
if (!agentDir || storeKey === mainStoreKey) {
|
||||
return store;
|
||||
}
|
||||
@@ -662,7 +667,7 @@ export function saveAuthProfileStore(
|
||||
agentDir?: string,
|
||||
options?: SaveAuthProfileStoreOptions,
|
||||
): void {
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir);
|
||||
const storeKey = resolveAuthProfileStoreKey(agentDir, options?.env);
|
||||
let updatedAt: number | null = null;
|
||||
let savedStore = store;
|
||||
runOpenClawStateWriteTransaction(
|
||||
|
||||
Reference in New Issue
Block a user