mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 15:24:58 +00:00
follow-up: align ingress, atomic paths, and channel tests with credential semantics (#33733)
Merged via squash.
Prepared head SHA: c290c2ab6a
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Co-authored-by: joshavant <830519+joshavant@users.noreply.github.com>
Reviewed-by: @joshavant
This commit is contained in:
@@ -51,7 +51,7 @@ function listSetupTokenProfiles(store: {
|
||||
if (normalizeProviderId(cred.provider) !== "anthropic") {
|
||||
return false;
|
||||
}
|
||||
return isSetupToken(cred.token);
|
||||
return isSetupToken(cred.token ?? "");
|
||||
})
|
||||
.map(([id]) => id);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ describe("buildAuthHealthSummary", () => {
|
||||
const now = 1_700_000_000_000;
|
||||
const profileStatuses = (summary: ReturnType<typeof buildAuthHealthSummary>) =>
|
||||
Object.fromEntries(summary.profiles.map((profile) => [profile.profileId, profile.status]));
|
||||
const profileReasonCodes = (summary: ReturnType<typeof buildAuthHealthSummary>) =>
|
||||
Object.fromEntries(summary.profiles.map((profile) => [profile.profileId, profile.reasonCode]));
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -89,6 +91,31 @@ describe("buildAuthHealthSummary", () => {
|
||||
|
||||
expect(statuses["google:no-refresh"]).toBe("expired");
|
||||
});
|
||||
|
||||
it("marks token profiles with invalid expires as missing with reason code", () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
const store = {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"github-copilot:invalid-expires": {
|
||||
type: "token" as const,
|
||||
provider: "github-copilot",
|
||||
token: "gh-token",
|
||||
expires: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const summary = buildAuthHealthSummary({
|
||||
store,
|
||||
warnAfterMs: DEFAULT_OAUTH_WARN_MS,
|
||||
});
|
||||
const statuses = profileStatuses(summary);
|
||||
const reasonCodes = profileReasonCodes(summary);
|
||||
|
||||
expect(statuses["github-copilot:invalid-expires"]).toBe("missing");
|
||||
expect(reasonCodes["github-copilot:invalid-expires"]).toBe("invalid_expires");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatRemainingShort", () => {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
type AuthCredentialReasonCode,
|
||||
type AuthProfileCredential,
|
||||
type AuthProfileStore,
|
||||
resolveAuthProfileDisplayLabel,
|
||||
} from "./auth-profiles.js";
|
||||
import {
|
||||
evaluateStoredCredentialEligibility,
|
||||
resolveTokenExpiryState,
|
||||
} from "./auth-profiles/credential-state.js";
|
||||
|
||||
export type AuthProfileSource = "store";
|
||||
|
||||
@@ -14,6 +19,7 @@ export type AuthProfileHealth = {
|
||||
provider: string;
|
||||
type: "oauth" | "token" | "api_key";
|
||||
status: AuthProfileHealthStatus;
|
||||
reasonCode?: AuthCredentialReasonCode;
|
||||
expiresAt?: number;
|
||||
remainingMs?: number;
|
||||
source: AuthProfileSource;
|
||||
@@ -113,11 +119,26 @@ function buildProfileHealth(params: {
|
||||
}
|
||||
|
||||
if (credential.type === "token") {
|
||||
const expiresAt =
|
||||
typeof credential.expires === "number" && Number.isFinite(credential.expires)
|
||||
? credential.expires
|
||||
: undefined;
|
||||
if (!expiresAt || expiresAt <= 0) {
|
||||
const eligibility = evaluateStoredCredentialEligibility({
|
||||
credential,
|
||||
now,
|
||||
});
|
||||
if (!eligibility.eligible) {
|
||||
const status: AuthProfileHealthStatus =
|
||||
eligibility.reasonCode === "expired" ? "expired" : "missing";
|
||||
return {
|
||||
profileId,
|
||||
provider: credential.provider,
|
||||
type: "token",
|
||||
status,
|
||||
reasonCode: eligibility.reasonCode,
|
||||
source,
|
||||
label,
|
||||
};
|
||||
}
|
||||
const expiryState = resolveTokenExpiryState(credential.expires, now);
|
||||
const expiresAt = expiryState === "valid" ? credential.expires : undefined;
|
||||
if (!expiresAt) {
|
||||
return {
|
||||
profileId,
|
||||
provider: credential.provider,
|
||||
@@ -133,6 +154,7 @@ function buildProfileHealth(params: {
|
||||
provider: credential.provider,
|
||||
type: "token",
|
||||
status,
|
||||
reasonCode: status === "expired" ? "expired" : undefined,
|
||||
expiresAt,
|
||||
remainingMs,
|
||||
source,
|
||||
|
||||
@@ -12,7 +12,8 @@ describe("resolveAuthProfileOrder", () => {
|
||||
function resolveMinimaxOrderWithProfile(profile: {
|
||||
type: "token";
|
||||
provider: "minimax";
|
||||
token: string;
|
||||
token?: string;
|
||||
tokenRef?: { source: "env" | "file" | "exec"; provider: string; id: string };
|
||||
expires?: number;
|
||||
}) {
|
||||
return resolveAuthProfileOrder({
|
||||
@@ -189,10 +190,79 @@ describe("resolveAuthProfileOrder", () => {
|
||||
expires: Date.now() - 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
caseName: "drops token profiles with invalid expires metadata",
|
||||
profile: {
|
||||
type: "token" as const,
|
||||
provider: "minimax" as const,
|
||||
token: "sk-minimax",
|
||||
expires: 0,
|
||||
},
|
||||
},
|
||||
])("$caseName", ({ profile }) => {
|
||||
const order = resolveMinimaxOrderWithProfile(profile);
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
it("keeps api_key profiles backed by keyRef when plaintext key is absent", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
auth: {
|
||||
order: {
|
||||
anthropic: ["anthropic:default"],
|
||||
},
|
||||
},
|
||||
},
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
"anthropic:default": {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
keyRef: {
|
||||
source: "exec",
|
||||
provider: "vault_local",
|
||||
id: "anthropic/default",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "anthropic",
|
||||
});
|
||||
expect(order).toEqual(["anthropic:default"]);
|
||||
});
|
||||
it("keeps token profiles backed by tokenRef when expires is absent", () => {
|
||||
const order = resolveMinimaxOrderWithProfile({
|
||||
type: "token",
|
||||
provider: "minimax",
|
||||
tokenRef: {
|
||||
source: "exec",
|
||||
provider: "keychain",
|
||||
id: "minimax/default",
|
||||
},
|
||||
});
|
||||
expect(order).toEqual(["minimax:default"]);
|
||||
});
|
||||
it("drops tokenRef profiles when expires is invalid", () => {
|
||||
const order = resolveMinimaxOrderWithProfile({
|
||||
type: "token",
|
||||
provider: "minimax",
|
||||
tokenRef: {
|
||||
source: "exec",
|
||||
provider: "keychain",
|
||||
id: "minimax/default",
|
||||
},
|
||||
expires: 0,
|
||||
});
|
||||
expect(order).toEqual([]);
|
||||
});
|
||||
it("keeps token profiles with inline token when no expires is set", () => {
|
||||
const order = resolveMinimaxOrderWithProfile({
|
||||
type: "token",
|
||||
provider: "minimax",
|
||||
token: "sk-minimax",
|
||||
});
|
||||
expect(order).toEqual(["minimax:default"]);
|
||||
});
|
||||
it("keeps oauth profiles that can refresh", () => {
|
||||
const order = resolveAuthProfileOrder({
|
||||
cfg: {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
export { CLAUDE_CLI_PROFILE_ID, CODEX_CLI_PROFILE_ID } from "./auth-profiles/constants.js";
|
||||
export type {
|
||||
AuthCredentialReasonCode,
|
||||
TokenExpiryState,
|
||||
} from "./auth-profiles/credential-state.js";
|
||||
export type { AuthProfileEligibilityReasonCode } from "./auth-profiles/order.js";
|
||||
export { resolveAuthProfileDisplayLabel } from "./auth-profiles/display.js";
|
||||
export { formatAuthDoctorHint } from "./auth-profiles/doctor.js";
|
||||
export { resolveApiKeyForProfile } from "./auth-profiles/oauth.js";
|
||||
export { resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||
export { resolveAuthProfileEligibility, resolveAuthProfileOrder } from "./auth-profiles/order.js";
|
||||
export { resolveAuthStorePathForDisplay } from "./auth-profiles/paths.js";
|
||||
export {
|
||||
dedupeProfileIds,
|
||||
|
||||
77
src/agents/auth-profiles/credential-state.test.ts
Normal file
77
src/agents/auth-profiles/credential-state.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
evaluateStoredCredentialEligibility,
|
||||
resolveTokenExpiryState,
|
||||
} from "./credential-state.js";
|
||||
|
||||
describe("resolveTokenExpiryState", () => {
|
||||
const now = 1_700_000_000_000;
|
||||
|
||||
it("treats undefined as missing", () => {
|
||||
expect(resolveTokenExpiryState(undefined, now)).toBe("missing");
|
||||
});
|
||||
|
||||
it("treats non-finite and non-positive values as invalid_expires", () => {
|
||||
expect(resolveTokenExpiryState(0, now)).toBe("invalid_expires");
|
||||
expect(resolveTokenExpiryState(-1, now)).toBe("invalid_expires");
|
||||
expect(resolveTokenExpiryState(Number.NaN, now)).toBe("invalid_expires");
|
||||
expect(resolveTokenExpiryState(Number.POSITIVE_INFINITY, now)).toBe("invalid_expires");
|
||||
});
|
||||
|
||||
it("returns expired when expires is in the past", () => {
|
||||
expect(resolveTokenExpiryState(now - 1, now)).toBe("expired");
|
||||
});
|
||||
|
||||
it("returns valid when expires is in the future", () => {
|
||||
expect(resolveTokenExpiryState(now + 1, now)).toBe("valid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("evaluateStoredCredentialEligibility", () => {
|
||||
const now = 1_700_000_000_000;
|
||||
|
||||
it("marks api_key with keyRef as eligible", () => {
|
||||
const result = evaluateStoredCredentialEligibility({
|
||||
credential: {
|
||||
type: "api_key",
|
||||
provider: "anthropic",
|
||||
keyRef: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "ANTHROPIC_API_KEY",
|
||||
},
|
||||
},
|
||||
now,
|
||||
});
|
||||
expect(result).toEqual({ eligible: true, reasonCode: "ok" });
|
||||
});
|
||||
|
||||
it("marks tokenRef with missing expires as eligible", () => {
|
||||
const result = evaluateStoredCredentialEligibility({
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
tokenRef: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "GITHUB_TOKEN",
|
||||
},
|
||||
},
|
||||
now,
|
||||
});
|
||||
expect(result).toEqual({ eligible: true, reasonCode: "ok" });
|
||||
});
|
||||
|
||||
it("marks token with invalid expires as ineligible", () => {
|
||||
const result = evaluateStoredCredentialEligibility({
|
||||
credential: {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
token: "tok",
|
||||
expires: 0,
|
||||
},
|
||||
now,
|
||||
});
|
||||
expect(result).toEqual({ eligible: false, reasonCode: "invalid_expires" });
|
||||
});
|
||||
});
|
||||
74
src/agents/auth-profiles/credential-state.ts
Normal file
74
src/agents/auth-profiles/credential-state.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { coerceSecretRef, normalizeSecretInputString } from "../../config/types.secrets.js";
|
||||
import type { AuthProfileCredential } from "./types.js";
|
||||
|
||||
export type AuthCredentialReasonCode =
|
||||
| "ok"
|
||||
| "missing_credential"
|
||||
| "invalid_expires"
|
||||
| "expired"
|
||||
| "unresolved_ref";
|
||||
|
||||
export type TokenExpiryState = "missing" | "valid" | "expired" | "invalid_expires";
|
||||
|
||||
export function resolveTokenExpiryState(expires: unknown, now = Date.now()): TokenExpiryState {
|
||||
if (expires === undefined) {
|
||||
return "missing";
|
||||
}
|
||||
if (typeof expires !== "number") {
|
||||
return "invalid_expires";
|
||||
}
|
||||
if (!Number.isFinite(expires) || expires <= 0) {
|
||||
return "invalid_expires";
|
||||
}
|
||||
return now >= expires ? "expired" : "valid";
|
||||
}
|
||||
|
||||
function hasConfiguredSecretRef(value: unknown): boolean {
|
||||
return coerceSecretRef(value) !== null;
|
||||
}
|
||||
|
||||
function hasConfiguredSecretString(value: unknown): boolean {
|
||||
return normalizeSecretInputString(value) !== undefined;
|
||||
}
|
||||
|
||||
export function evaluateStoredCredentialEligibility(params: {
|
||||
credential: AuthProfileCredential;
|
||||
now?: number;
|
||||
}): { eligible: boolean; reasonCode: AuthCredentialReasonCode } {
|
||||
const now = params.now ?? Date.now();
|
||||
const credential = params.credential;
|
||||
|
||||
if (credential.type === "api_key") {
|
||||
const hasKey = hasConfiguredSecretString(credential.key);
|
||||
const hasKeyRef = hasConfiguredSecretRef(credential.keyRef);
|
||||
if (!hasKey && !hasKeyRef) {
|
||||
return { eligible: false, reasonCode: "missing_credential" };
|
||||
}
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
|
||||
if (credential.type === "token") {
|
||||
const hasToken = hasConfiguredSecretString(credential.token);
|
||||
const hasTokenRef = hasConfiguredSecretRef(credential.tokenRef);
|
||||
if (!hasToken && !hasTokenRef) {
|
||||
return { eligible: false, reasonCode: "missing_credential" };
|
||||
}
|
||||
|
||||
const expiryState = resolveTokenExpiryState(credential.expires, now);
|
||||
if (expiryState === "invalid_expires") {
|
||||
return { eligible: false, reasonCode: "invalid_expires" };
|
||||
}
|
||||
if (expiryState === "expired") {
|
||||
return { eligible: false, reasonCode: "expired" };
|
||||
}
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
|
||||
if (
|
||||
normalizeSecretInputString(credential.access) === undefined &&
|
||||
normalizeSecretInputString(credential.refresh) === undefined
|
||||
) {
|
||||
return { eligible: false, reasonCode: "missing_credential" };
|
||||
}
|
||||
return { eligible: true, reasonCode: "ok" };
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function cfgFor(profileId: string, provider: string, mode: "api_key" | "token" |
|
||||
function tokenStore(params: {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
token: string;
|
||||
token?: string;
|
||||
expires?: number;
|
||||
}): AuthProfileStore {
|
||||
return {
|
||||
@@ -132,6 +132,45 @@ describe("resolveApiKeyForProfile config compatibility", () => {
|
||||
});
|
||||
|
||||
describe("resolveApiKeyForProfile token expiry handling", () => {
|
||||
it("accepts token credentials when expires is undefined", async () => {
|
||||
const profileId = "anthropic:token-no-expiry";
|
||||
const result = await resolveWithConfig({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
store: tokenStore({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
token: "tok-123",
|
||||
}),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "tok-123",
|
||||
provider: "anthropic",
|
||||
email: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("accepts token credentials when expires is in the future", async () => {
|
||||
const profileId = "anthropic:token-valid-expiry";
|
||||
const result = await resolveWithConfig({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
store: tokenStore({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
token: "tok-123",
|
||||
expires: Date.now() + 60_000,
|
||||
}),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "tok-123",
|
||||
provider: "anthropic",
|
||||
email: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null for expired token credentials", async () => {
|
||||
const profileId = "anthropic:token-expired";
|
||||
const result = await resolveWithConfig({
|
||||
@@ -148,7 +187,7 @@ describe("resolveApiKeyForProfile token expiry handling", () => {
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("accepts token credentials when expires is 0", async () => {
|
||||
it("returns null for token credentials when expires is 0", async () => {
|
||||
const profileId = "anthropic:token-no-expiry";
|
||||
const result = await resolveWithConfig({
|
||||
profileId,
|
||||
@@ -161,11 +200,30 @@ describe("resolveApiKeyForProfile token expiry handling", () => {
|
||||
expires: 0,
|
||||
}),
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "tok-123",
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for token credentials when expires is invalid (NaN)", async () => {
|
||||
const profileId = "anthropic:token-invalid-expiry";
|
||||
const store = tokenStore({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
email: undefined,
|
||||
token: "tok-123",
|
||||
});
|
||||
store.profiles[profileId] = {
|
||||
...store.profiles[profileId],
|
||||
type: "token",
|
||||
provider: "anthropic",
|
||||
token: "tok-123",
|
||||
expires: Number.NaN,
|
||||
};
|
||||
const result = await resolveWithConfig({
|
||||
profileId,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
store,
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -237,6 +295,39 @@ describe("resolveApiKeyForProfile secret refs", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves token tokenRef without inline token when expires is absent", async () => {
|
||||
const profileId = "github-copilot:no-inline-token";
|
||||
const previous = process.env.GITHUB_TOKEN;
|
||||
process.env.GITHUB_TOKEN = "gh-ref-token";
|
||||
try {
|
||||
const result = await resolveApiKeyForProfile({
|
||||
cfg: cfgFor(profileId, "github-copilot", "token"),
|
||||
store: {
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
tokenRef: { source: "env", provider: "default", id: "GITHUB_TOKEN" },
|
||||
},
|
||||
},
|
||||
},
|
||||
profileId,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
apiKey: "gh-ref-token",
|
||||
provider: "github-copilot",
|
||||
email: undefined,
|
||||
});
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
} else {
|
||||
process.env.GITHUB_TOKEN = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves inline ${ENV} api_key values", async () => {
|
||||
const profileId = "openai:inline-env";
|
||||
const previous = process.env.OPENAI_API_KEY;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.
|
||||
import { resolveSecretRefString, type SecretRefResolveCache } from "../../secrets/resolve.js";
|
||||
import { refreshChutesTokens } from "../chutes-oauth.js";
|
||||
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
import { resolveTokenExpiryState } from "./credential-state.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||
@@ -86,12 +87,6 @@ function buildOAuthProfileResult(params: {
|
||||
});
|
||||
}
|
||||
|
||||
function isExpiredCredential(expires: number | undefined): boolean {
|
||||
return (
|
||||
typeof expires === "number" && Number.isFinite(expires) && expires > 0 && Date.now() >= expires
|
||||
);
|
||||
}
|
||||
|
||||
type ResolveApiKeyForProfileParams = {
|
||||
cfg?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
@@ -332,6 +327,10 @@ export async function resolveApiKeyForProfile(
|
||||
return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email });
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
const expiryState = resolveTokenExpiryState(cred.expires);
|
||||
if (expiryState === "expired" || expiryState === "invalid_expires") {
|
||||
return null;
|
||||
}
|
||||
const token = await resolveProfileSecretString({
|
||||
profileId,
|
||||
provider: cred.provider,
|
||||
@@ -346,9 +345,6 @@ export async function resolveApiKeyForProfile(
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
if (isExpiredCredential(cred.expires)) {
|
||||
return null;
|
||||
}
|
||||
return buildApiKeyProfileResult({ apiKey: token, provider: cred.provider, email: cred.email });
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import {
|
||||
normalizeProviderId,
|
||||
normalizeProviderIdForAuth,
|
||||
} from "../model-selection.js";
|
||||
import {
|
||||
evaluateStoredCredentialEligibility,
|
||||
type AuthCredentialReasonCode,
|
||||
} from "./credential-state.js";
|
||||
import { dedupeProfileIds, listProfilesForProvider } from "./profiles.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
import {
|
||||
@@ -12,6 +16,54 @@ import {
|
||||
resolveProfileUnusableUntil,
|
||||
} from "./usage.js";
|
||||
|
||||
export type AuthProfileEligibilityReasonCode =
|
||||
| AuthCredentialReasonCode
|
||||
| "profile_missing"
|
||||
| "provider_mismatch"
|
||||
| "mode_mismatch";
|
||||
|
||||
export type AuthProfileEligibility = {
|
||||
eligible: boolean;
|
||||
reasonCode: AuthProfileEligibilityReasonCode;
|
||||
};
|
||||
|
||||
export function resolveAuthProfileEligibility(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
provider: string;
|
||||
profileId: string;
|
||||
now?: number;
|
||||
}): AuthProfileEligibility {
|
||||
const providerAuthKey = normalizeProviderIdForAuth(params.provider);
|
||||
const cred = params.store.profiles[params.profileId];
|
||||
if (!cred) {
|
||||
return { eligible: false, reasonCode: "profile_missing" };
|
||||
}
|
||||
if (normalizeProviderIdForAuth(cred.provider) !== providerAuthKey) {
|
||||
return { eligible: false, reasonCode: "provider_mismatch" };
|
||||
}
|
||||
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||
if (profileConfig) {
|
||||
if (normalizeProviderIdForAuth(profileConfig.provider) !== providerAuthKey) {
|
||||
return { eligible: false, reasonCode: "provider_mismatch" };
|
||||
}
|
||||
if (profileConfig.mode !== cred.type) {
|
||||
const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token";
|
||||
if (!oauthCompatible) {
|
||||
return { eligible: false, reasonCode: "mode_mismatch" };
|
||||
}
|
||||
}
|
||||
}
|
||||
const credentialEligibility = evaluateStoredCredentialEligibility({
|
||||
credential: cred,
|
||||
now: params.now,
|
||||
});
|
||||
return {
|
||||
eligible: credentialEligibility.eligible,
|
||||
reasonCode: credentialEligibility.reasonCode,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveAuthProfileOrder(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
store: AuthProfileStore;
|
||||
@@ -42,48 +94,14 @@ export function resolveAuthProfileOrder(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
const isValidProfile = (profileId: string): boolean => {
|
||||
const cred = store.profiles[profileId];
|
||||
if (!cred) {
|
||||
return false;
|
||||
}
|
||||
if (normalizeProviderIdForAuth(cred.provider) !== providerAuthKey) {
|
||||
return false;
|
||||
}
|
||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
||||
if (profileConfig) {
|
||||
if (normalizeProviderIdForAuth(profileConfig.provider) !== providerAuthKey) {
|
||||
return false;
|
||||
}
|
||||
if (profileConfig.mode !== cred.type) {
|
||||
const oauthCompatible = profileConfig.mode === "oauth" && cred.type === "token";
|
||||
if (!oauthCompatible) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cred.type === "api_key") {
|
||||
return Boolean(cred.key?.trim());
|
||||
}
|
||||
if (cred.type === "token") {
|
||||
if (!cred.token?.trim()) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
typeof cred.expires === "number" &&
|
||||
Number.isFinite(cred.expires) &&
|
||||
cred.expires > 0 &&
|
||||
now >= cred.expires
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (cred.type === "oauth") {
|
||||
return Boolean(cred.access?.trim() || cred.refresh?.trim());
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const isValidProfile = (profileId: string): boolean =>
|
||||
resolveAuthProfileEligibility({
|
||||
cfg,
|
||||
store,
|
||||
provider: providerAuthKey,
|
||||
profileId,
|
||||
now,
|
||||
}).eligible;
|
||||
let filtered = baseOrder.filter(isValidProfile);
|
||||
|
||||
// Repair config/store profile-id drift from older onboarding flows:
|
||||
|
||||
@@ -19,7 +19,7 @@ export type TokenCredential = {
|
||||
*/
|
||||
type: "token";
|
||||
provider: string;
|
||||
token: string;
|
||||
token?: string;
|
||||
tokenRef?: SecretRef;
|
||||
/** Optional expiry timestamp (ms since epoch). */
|
||||
expires?: number;
|
||||
|
||||
Reference in New Issue
Block a user