mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:20:43 +00:00
refactor(auth): break oauth helper import cycle
This commit is contained in:
committed by
Peter Steinberger
parent
20debfab90
commit
f6921fd733
@@ -5,7 +5,7 @@ import {
|
||||
overlayRuntimeExternalOAuthProfiles,
|
||||
shouldPersistRuntimeExternalOAuthProfile,
|
||||
type RuntimeExternalOAuthProfile,
|
||||
} from "./oauth-manager.js";
|
||||
} from "./oauth-shared.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
type ExternalAuthProfileMap = Map<string, ProviderExternalAuthProfile>;
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
isSafeToOverwriteStoredOAuthIdentity,
|
||||
shouldBootstrapFromExternalCliCredential,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
} from "./oauth-manager.js";
|
||||
} from "./oauth-shared.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
export {
|
||||
@@ -18,7 +18,7 @@ export {
|
||||
isSafeToOverwriteStoredOAuthIdentity,
|
||||
shouldBootstrapFromExternalCliCredential,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
} from "./oauth-manager.js";
|
||||
} from "./oauth-shared.js";
|
||||
|
||||
export type ExternalCliResolvedProfile = {
|
||||
profileId: string;
|
||||
|
||||
@@ -124,7 +124,7 @@ describe("auth external oauth helpers", () => {
|
||||
expect(shouldPersist).toBe(true);
|
||||
});
|
||||
|
||||
it("overlays external CLI OAuth only when the stored credential is no longer usable", () => {
|
||||
it("does not use Codex CLI OAuth as a runtime overlay source", () => {
|
||||
readCodexCliCredentialsCachedMock.mockReturnValue(
|
||||
createCredential({
|
||||
access: "fresh-cli-access-token",
|
||||
@@ -146,9 +146,9 @@ describe("auth external oauth helpers", () => {
|
||||
);
|
||||
|
||||
expect(overlaid.profiles["openai-codex:default"]).toMatchObject({
|
||||
access: "fresh-cli-access-token",
|
||||
refresh: "fresh-cli-refresh-token",
|
||||
expires: expect.any(Number),
|
||||
access: "stale-store-access-token",
|
||||
refresh: "stale-store-refresh-token",
|
||||
accountId: "acct-cli",
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,17 @@ import {
|
||||
OAUTH_REFRESH_LOCK_OPTIONS,
|
||||
log,
|
||||
} from "./constants.js";
|
||||
import { hasUsableOAuthCredential as hasUsableStoredOAuthCredential } from "./credential-state.js";
|
||||
import {
|
||||
areOAuthCredentialsEquivalent,
|
||||
hasUsableOAuthCredential,
|
||||
isSafeToAdoptBootstrapOAuthIdentity,
|
||||
isSafeToOverwriteStoredOAuthIdentity,
|
||||
overlayRuntimeExternalOAuthProfiles,
|
||||
shouldBootstrapFromExternalCliCredential,
|
||||
shouldPersistRuntimeExternalOAuthProfile,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
type RuntimeExternalOAuthProfile,
|
||||
} from "./oauth-shared.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath, resolveOAuthRefreshLockPath } from "./paths.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
@@ -76,179 +86,17 @@ export class OAuthManagerRefreshError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export type RuntimeExternalOAuthProfile = {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
persistence?: "runtime-only" | "persisted";
|
||||
export {
|
||||
areOAuthCredentialsEquivalent,
|
||||
hasUsableOAuthCredential,
|
||||
isSafeToAdoptBootstrapOAuthIdentity,
|
||||
isSafeToOverwriteStoredOAuthIdentity,
|
||||
overlayRuntimeExternalOAuthProfiles,
|
||||
shouldBootstrapFromExternalCliCredential,
|
||||
shouldPersistRuntimeExternalOAuthProfile,
|
||||
shouldReplaceStoredOAuthCredential,
|
||||
};
|
||||
|
||||
export function areOAuthCredentialsEquivalent(
|
||||
a: OAuthCredential | undefined,
|
||||
b: OAuthCredential,
|
||||
): boolean {
|
||||
if (!a || a.type !== "oauth") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
a.provider === b.provider &&
|
||||
a.access === b.access &&
|
||||
a.refresh === b.refresh &&
|
||||
a.expires === b.expires &&
|
||||
a.email === b.email &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId
|
||||
);
|
||||
}
|
||||
|
||||
function hasNewerStoredOAuthCredential(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
existing &&
|
||||
existing.provider === incoming.provider &&
|
||||
Number.isFinite(existing.expires) &&
|
||||
(!Number.isFinite(incoming.expires) || existing.expires > incoming.expires),
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldReplaceStoredOAuthCredential(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return false;
|
||||
}
|
||||
return !hasNewerStoredOAuthCredential(existing, incoming);
|
||||
}
|
||||
|
||||
export function hasUsableOAuthCredential(
|
||||
credential: OAuthCredential | undefined,
|
||||
now = Date.now(),
|
||||
): boolean {
|
||||
return hasUsableStoredOAuthCredential(credential, { now });
|
||||
}
|
||||
|
||||
function normalizeAuthIdentityToken(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeAuthEmailToken(value: string | undefined): string | undefined {
|
||||
return normalizeAuthIdentityToken(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
function hasOAuthIdentity(credential: Pick<OAuthCredential, "accountId" | "email">): boolean {
|
||||
return (
|
||||
normalizeAuthIdentityToken(credential.accountId) !== undefined ||
|
||||
normalizeAuthEmailToken(credential.email) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function hasMatchingOAuthIdentity(
|
||||
existing: Pick<OAuthCredential, "accountId" | "email">,
|
||||
incoming: Pick<OAuthCredential, "accountId" | "email">,
|
||||
): boolean {
|
||||
const existingAccountId = normalizeAuthIdentityToken(existing.accountId);
|
||||
const incomingAccountId = normalizeAuthIdentityToken(incoming.accountId);
|
||||
if (existingAccountId !== undefined && incomingAccountId !== undefined) {
|
||||
return existingAccountId === incomingAccountId;
|
||||
}
|
||||
|
||||
const existingEmail = normalizeAuthEmailToken(existing.email);
|
||||
const incomingEmail = normalizeAuthEmailToken(incoming.email);
|
||||
if (existingEmail !== undefined && incomingEmail !== undefined) {
|
||||
return existingEmail === incomingEmail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSafeToOverwriteStoredOAuthIdentity(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (existing.provider !== incoming.provider) {
|
||||
return false;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasOAuthIdentity(existing)) {
|
||||
return false;
|
||||
}
|
||||
return hasMatchingOAuthIdentity(existing, incoming);
|
||||
}
|
||||
|
||||
export function isSafeToAdoptBootstrapOAuthIdentity(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (existing.provider !== incoming.provider) {
|
||||
return false;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasOAuthIdentity(existing)) {
|
||||
return true;
|
||||
}
|
||||
return hasMatchingOAuthIdentity(existing, incoming);
|
||||
}
|
||||
|
||||
export function shouldBootstrapFromExternalCliCredential(params: {
|
||||
existing: OAuthCredential | undefined;
|
||||
imported: OAuthCredential;
|
||||
now?: number;
|
||||
}): boolean {
|
||||
const now = params.now ?? Date.now();
|
||||
if (hasUsableOAuthCredential(params.existing, now)) {
|
||||
return false;
|
||||
}
|
||||
return hasUsableOAuthCredential(params.imported, now);
|
||||
}
|
||||
|
||||
export function overlayRuntimeExternalOAuthProfiles(
|
||||
store: AuthProfileStore,
|
||||
profiles: Iterable<RuntimeExternalOAuthProfile>,
|
||||
): AuthProfileStore {
|
||||
const externalProfiles = Array.from(profiles);
|
||||
if (externalProfiles.length === 0) {
|
||||
return store;
|
||||
}
|
||||
const next = structuredClone(store);
|
||||
for (const profile of externalProfiles) {
|
||||
next.profiles[profile.profileId] = profile.credential;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shouldPersistRuntimeExternalOAuthProfile(params: {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
profiles: Iterable<RuntimeExternalOAuthProfile>;
|
||||
}): boolean {
|
||||
for (const profile of params.profiles) {
|
||||
if (profile.profileId !== params.profileId) {
|
||||
continue;
|
||||
}
|
||||
if (profile.persistence === "persisted") {
|
||||
return true;
|
||||
}
|
||||
return !areOAuthCredentialsEquivalent(profile.credential, params.credential);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
export type { RuntimeExternalOAuthProfile };
|
||||
|
||||
function hasOAuthCredentialChanged(
|
||||
previous: Pick<OAuthCredential, "access" | "refresh" | "expires">,
|
||||
|
||||
176
src/agents/auth-profiles/oauth-shared.ts
Normal file
176
src/agents/auth-profiles/oauth-shared.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { hasUsableOAuthCredential as hasUsableStoredOAuthCredential } from "./credential-state.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
|
||||
export type RuntimeExternalOAuthProfile = {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
persistence?: "runtime-only" | "persisted";
|
||||
};
|
||||
|
||||
export function areOAuthCredentialsEquivalent(
|
||||
a: OAuthCredential | undefined,
|
||||
b: OAuthCredential,
|
||||
): boolean {
|
||||
if (!a || a.type !== "oauth") {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
a.provider === b.provider &&
|
||||
a.access === b.access &&
|
||||
a.refresh === b.refresh &&
|
||||
a.expires === b.expires &&
|
||||
a.email === b.email &&
|
||||
a.enterpriseUrl === b.enterpriseUrl &&
|
||||
a.projectId === b.projectId &&
|
||||
a.accountId === b.accountId
|
||||
);
|
||||
}
|
||||
|
||||
function hasNewerStoredOAuthCredential(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
return Boolean(
|
||||
existing &&
|
||||
existing.provider === incoming.provider &&
|
||||
Number.isFinite(existing.expires) &&
|
||||
(!Number.isFinite(incoming.expires) || existing.expires > incoming.expires),
|
||||
);
|
||||
}
|
||||
|
||||
export function shouldReplaceStoredOAuthCredential(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return false;
|
||||
}
|
||||
return !hasNewerStoredOAuthCredential(existing, incoming);
|
||||
}
|
||||
|
||||
export function hasUsableOAuthCredential(
|
||||
credential: OAuthCredential | undefined,
|
||||
now = Date.now(),
|
||||
): boolean {
|
||||
return hasUsableStoredOAuthCredential(credential, { now });
|
||||
}
|
||||
|
||||
function normalizeAuthIdentityToken(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function normalizeAuthEmailToken(value: string | undefined): string | undefined {
|
||||
return normalizeAuthIdentityToken(value)?.toLowerCase();
|
||||
}
|
||||
|
||||
function hasOAuthIdentity(credential: Pick<OAuthCredential, "accountId" | "email">): boolean {
|
||||
return (
|
||||
normalizeAuthIdentityToken(credential.accountId) !== undefined ||
|
||||
normalizeAuthEmailToken(credential.email) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
function hasMatchingOAuthIdentity(
|
||||
existing: Pick<OAuthCredential, "accountId" | "email">,
|
||||
incoming: Pick<OAuthCredential, "accountId" | "email">,
|
||||
): boolean {
|
||||
const existingAccountId = normalizeAuthIdentityToken(existing.accountId);
|
||||
const incomingAccountId = normalizeAuthIdentityToken(incoming.accountId);
|
||||
if (existingAccountId !== undefined && incomingAccountId !== undefined) {
|
||||
return existingAccountId === incomingAccountId;
|
||||
}
|
||||
|
||||
const existingEmail = normalizeAuthEmailToken(existing.email);
|
||||
const incomingEmail = normalizeAuthEmailToken(incoming.email);
|
||||
if (existingEmail !== undefined && incomingEmail !== undefined) {
|
||||
return existingEmail === incomingEmail;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isSafeToOverwriteStoredOAuthIdentity(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (existing.provider !== incoming.provider) {
|
||||
return false;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasOAuthIdentity(existing)) {
|
||||
return false;
|
||||
}
|
||||
return hasMatchingOAuthIdentity(existing, incoming);
|
||||
}
|
||||
|
||||
export function isSafeToAdoptBootstrapOAuthIdentity(
|
||||
existing: OAuthCredential | undefined,
|
||||
incoming: OAuthCredential,
|
||||
): boolean {
|
||||
if (!existing || existing.type !== "oauth") {
|
||||
return true;
|
||||
}
|
||||
if (existing.provider !== incoming.provider) {
|
||||
return false;
|
||||
}
|
||||
if (areOAuthCredentialsEquivalent(existing, incoming)) {
|
||||
return true;
|
||||
}
|
||||
if (!hasOAuthIdentity(existing)) {
|
||||
return true;
|
||||
}
|
||||
return hasMatchingOAuthIdentity(existing, incoming);
|
||||
}
|
||||
|
||||
export function shouldBootstrapFromExternalCliCredential(params: {
|
||||
existing: OAuthCredential | undefined;
|
||||
imported: OAuthCredential;
|
||||
now?: number;
|
||||
}): boolean {
|
||||
const now = params.now ?? Date.now();
|
||||
if (hasUsableOAuthCredential(params.existing, now)) {
|
||||
return false;
|
||||
}
|
||||
return hasUsableOAuthCredential(params.imported, now);
|
||||
}
|
||||
|
||||
export function overlayRuntimeExternalOAuthProfiles(
|
||||
store: AuthProfileStore,
|
||||
profiles: Iterable<RuntimeExternalOAuthProfile>,
|
||||
): AuthProfileStore {
|
||||
const externalProfiles = Array.from(profiles);
|
||||
if (externalProfiles.length === 0) {
|
||||
return store;
|
||||
}
|
||||
const next = structuredClone(store);
|
||||
for (const profile of externalProfiles) {
|
||||
next.profiles[profile.profileId] = profile.credential;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
export function shouldPersistRuntimeExternalOAuthProfile(params: {
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
profiles: Iterable<RuntimeExternalOAuthProfile>;
|
||||
}): boolean {
|
||||
for (const profile of params.profiles) {
|
||||
if (profile.profileId !== params.profileId) {
|
||||
continue;
|
||||
}
|
||||
if (profile.persistence === "persisted") {
|
||||
return true;
|
||||
}
|
||||
return !areOAuthCredentialsEquivalent(profile.credential, params.credential);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user