diff --git a/src/agents/auth-health.test.ts b/src/agents/auth-health.test.ts index 3b416545904..287e6be2d12 100644 --- a/src/agents/auth-health.test.ts +++ b/src/agents/auth-health.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { MAX_DATE_TIMESTAMP_MS } from "../shared/number-coercion.js"; import type { OAuthCredential } from "./auth-profiles/types.js"; const { readCodexCliCredentialsCachedMock } = vi.hoisted(() => ({ @@ -376,6 +377,35 @@ describe("buildAuthHealthSummary", () => { expect(reasonCodes["github-copilot:invalid-expires"]).toBe("invalid_expires"); }); + it("does not expose out-of-range oauth expiry values in health rollups", () => { + vi.spyOn(Date, "now").mockReturnValue(now); + const store = { + version: 1, + profiles: { + "openai-codex:bad-expiry": { + type: "oauth" as const, + provider: "openai-codex", + access: "oauth-access", + refresh: "oauth-refresh", + expires: MAX_DATE_TIMESTAMP_MS + 1, + }, + }, + }; + + const summary = buildAuthHealthSummary({ + store, + warnAfterMs: DEFAULT_OAUTH_WARN_MS, + }); + + const profile = summary.profiles.find((entry) => entry.profileId === "openai-codex:bad-expiry"); + const provider = summary.providers.find((entry) => entry.provider === "openai-codex"); + + expect(profile?.status).toBe("missing"); + expect(profile?.expiresAt).toBeUndefined(); + expect(provider?.status).toBe("missing"); + expect(provider?.expiresAt).toBeUndefined(); + }); + it("does not normalize provider aliases when filtering and grouping profile health", () => { vi.spyOn(Date, "now").mockReturnValue(now); const store = { diff --git a/src/agents/auth-health.ts b/src/agents/auth-health.ts index fe142e45404..ef655f1cf78 100644 --- a/src/agents/auth-health.ts +++ b/src/agents/auth-health.ts @@ -3,6 +3,7 @@ import { normalizeProviderId, } from "@openclaw/model-catalog-core/provider-id"; import type { OpenClawConfig } from "../config/types.openclaw.js"; +import { asDateTimestampMs } from "../shared/number-coercion.js"; import { normalizeUniqueStringEntries } from "../shared/string-normalization.js"; import { DEFAULT_OAUTH_REFRESH_MARGIN_MS, @@ -92,24 +93,25 @@ function resolveOAuthStatus( expiresAt: number | undefined, now: number, expiringWithinMs: number, -): { status: AuthProfileHealthStatus; remainingMs?: number } { - if (!expiresAt || !Number.isFinite(expiresAt) || expiresAt <= 0) { +): { status: AuthProfileHealthStatus; expiresAt?: number; remainingMs?: number } { + const normalizedExpiresAt = asDateTimestampMs(expiresAt); + if (normalizedExpiresAt === undefined || normalizedExpiresAt <= 0) { return { status: "missing" }; } - const remainingMs = expiresAt - now; - const expiryState = resolveTokenExpiryState(expiresAt, now, { + const remainingMs = normalizedExpiresAt - now; + const expiryState = resolveTokenExpiryState(normalizedExpiresAt, now, { expiringWithinMs, }); if (expiryState === "invalid_expires" || expiryState === "missing") { return { status: "missing" }; } if (expiryState === "expired") { - return { status: "expired", remainingMs }; + return { status: "expired", expiresAt: normalizedExpiresAt, remainingMs }; } if (expiryState === "expiring") { - return { status: "expiring", remainingMs }; + return { status: "expiring", expiresAt: normalizedExpiresAt, remainingMs }; } - return { status: "ok", remainingMs }; + return { status: "ok", expiresAt: normalizedExpiresAt, remainingMs }; } function buildProfileHealth(params: { @@ -168,14 +170,18 @@ function buildProfileHealth(params: { label, }; } - const { status, remainingMs } = resolveOAuthStatus(expiresAt, now, warnAfterMs); + const { + status, + expiresAt: normalizedExpiresAt, + remainingMs, + } = resolveOAuthStatus(expiresAt, now, warnAfterMs); return { profileId, provider, type: "token", status, reasonCode: status === "expired" ? "expired" : undefined, - expiresAt, + expiresAt: normalizedExpiresAt, remainingMs, source, label, @@ -187,17 +193,17 @@ function buildProfileHealth(params: { credential: healthCredential, }); const oauthWarnAfterMs = Math.max(warnAfterMs, DEFAULT_OAUTH_REFRESH_MARGIN_MS); - const { status: rawStatus, remainingMs } = resolveOAuthStatus( - effectiveCredential.expires, - now, - oauthWarnAfterMs, - ); + const { + status: rawStatus, + expiresAt, + remainingMs, + } = resolveOAuthStatus(effectiveCredential.expires, now, oauthWarnAfterMs); return { profileId, provider, type: "oauth", status: rawStatus, - expiresAt: effectiveCredential.expires, + expiresAt, remainingMs, source, label, @@ -317,7 +323,8 @@ export function buildAuthHealthSummary(params: { let hasApiKeyProfile = false; let hasExpirableProfile = false; - let hasExpiredOrMissing = false; + let hasExpired = false; + let hasMissing = false; let hasExpiring = false; let earliestExpiry: number | undefined; for (const profile of effectiveProfiles) { @@ -335,8 +342,10 @@ export function buildAuthHealthSummary(params: { ? profile.expiresAt : Math.min(earliestExpiry, profile.expiresAt); } - if (profile.status === "expired" || profile.status === "missing") { - hasExpiredOrMissing = true; + if (profile.status === "expired") { + hasExpired = true; + } else if (profile.status === "missing") { + hasMissing = true; } else if (profile.status === "expiring") { hasExpiring = true; } @@ -352,8 +361,10 @@ export function buildAuthHealthSummary(params: { provider.remainingMs = provider.expiresAt - now; } - if (hasExpiredOrMissing) { + if (hasExpired) { provider.status = "expired"; + } else if (hasMissing) { + provider.status = "missing"; } else if (hasExpiring) { provider.status = "expiring"; } else {