File size: 6,074 Bytes
35f99ee | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 | /** Converts auth-profile credentials into agent runtime credential maps. */
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { asDateTimestampMs } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { coerceSecretRef } from "../config/types.secrets.js";
import type {
PreparedAgentCredentialMode,
PreparedAgentCredentialModes,
} from "./agent-auth-credential-modes.js";
import { isOAuthRefreshFence } from "./auth-profiles/oauth-refresh-marker.js";
import { resolveAuthProfileOrder } from "./auth-profiles/order.js";
import type { AuthProfileCredential, AuthProfileStore } from "./auth-profiles/types.js";
import type { ApiKeyCredential, AuthStorageData } from "./sessions/auth-storage.js";
// Converts auth-profile credentials into the compact credential map consumed by
// agent runtimes. Secret refs can be represented by markers without reading
// secret values.
type AgentApiKeyCredential = ApiKeyCredential;
type AgentOAuthCredential = {
type: "oauth";
access: string;
refresh: string;
expires: number;
};
/** Credential value shape consumed by agent runtimes after auth-profile normalization. */
type AgentCredential = AgentApiKeyCredential | AgentOAuthCredential;
export type AgentCredentialMap = Record<string, AgentCredential>;
type ResolveAgentCredentialMapOptions = {
includeSecretRefPlaceholders?: boolean;
config?: OpenClawConfig;
};
const AGENT_SECRET_REF_CONFIGURED_MARKER = "openclaw-secret-ref-configured";
/** Records only credential modes whose secret material is usable by a prepared runtime owner. */
export function resolveUsableAgentCredentialModes(
credentials: Readonly<AuthStorageData>,
): PreparedAgentCredentialModes {
const modes: Record<string, PreparedAgentCredentialMode> = {};
for (const [rawProvider, credential] of Object.entries(credentials)) {
const provider = normalizeProviderId(rawProvider);
if (!provider) {
continue;
}
if (
credential.type === "api_key" &&
credential.key &&
credential.key !== AGENT_SECRET_REF_CONFIGURED_MARKER
) {
const native = credential.nativeAuth;
if (!native || normalizeProviderId(native.runtime) === provider) {
modes[provider] = native
? Object.freeze({
source: "native",
mode: native.mode === "api-key" ? "api_key" : native.mode,
})
: "api_key";
}
} else if (
credential.type === "token" &&
credential.token &&
(credential.expires === undefined || credential.expires > Date.now())
) {
modes[provider] = "token";
} else if (
credential.type === "oauth" &&
!isOAuthRefreshFence(credential) &&
credential.access &&
credential.refresh &&
credential.expires > 0
) {
modes[provider] = "oauth";
}
}
return Object.freeze(modes);
}
function hasConfiguredSecretRef(value: unknown): boolean {
return coerceSecretRef(value) !== null;
}
function secretRefPlaceholder(
options: ResolveAgentCredentialMapOptions | undefined,
): AgentCredential | null {
if (options?.includeSecretRefPlaceholders === true) {
return { type: "api_key", key: AGENT_SECRET_REF_CONFIGURED_MARKER };
}
return null;
}
function convertAuthProfileCredentialToAgent(
cred: AuthProfileCredential,
options?: ResolveAgentCredentialMapOptions,
): AgentCredential | null {
if (cred.type === "api_key") {
const key = normalizeOptionalString(cred.key) ?? "";
if (!key) {
// A configured secret ref proves the credential exists, but this converter
// must not resolve or leak the actual secret value.
return hasConfiguredSecretRef(cred.keyRef) ? secretRefPlaceholder(options) : null;
}
return { type: "api_key", key };
}
if (cred.type === "token") {
if (cred.expires !== undefined) {
const expires = asDateTimestampMs(cred.expires);
if (expires === undefined || Date.now() >= expires) {
return null;
}
}
const token = normalizeOptionalString(cred.token) ?? "";
if (!token) {
return hasConfiguredSecretRef(cred.tokenRef) ? secretRefPlaceholder(options) : null;
}
return { type: "api_key", key: token };
}
if (cred.type === "oauth") {
if (isOAuthRefreshFence(cred)) {
return null;
}
const access = normalizeOptionalString(cred.access) ?? "";
const refresh = normalizeOptionalString(cred.refresh) ?? "";
const expires = asDateTimestampMs(cred.expires);
if (!access || !refresh || expires === undefined || expires <= 0) {
return null;
}
return {
type: "oauth",
access,
refresh,
expires,
};
}
return null;
}
/** Build one canonically selected credential per normalized provider. */
export function resolveAgentCredentialMapFromStore(
store: AuthProfileStore,
options?: ResolveAgentCredentialMapOptions,
): AgentCredentialMap {
const credentials: AgentCredentialMap = {};
for (const credential of Object.values(store.profiles)) {
const provider = normalizeProviderId(credential.provider ?? "");
if (!provider) {
continue;
}
if (credentials[provider]) {
continue;
}
// Discovery must not grow a second auth policy: explicit order, provider
// aliases, eligibility, and automatic preference all belong to this resolver.
const profileIds = resolveAuthProfileOrder({
cfg: options?.config,
store,
provider,
...(options?.includeSecretRefPlaceholders === true ? { readinessMode: "read-only" } : {}),
});
for (const profileId of profileIds) {
const profile = store.profiles[profileId];
if (!profile) {
continue;
}
const converted = convertAuthProfileCredentialToAgent(profile, options);
if (converted) {
credentials[provider] = converted;
break;
}
}
}
return credentials;
}
|