fix(google): bound vertex adc token cache expiry

This commit is contained in:
Peter Steinberger
2026-05-30 11:52:19 -04:00
parent 77761f4a3e
commit 697bafa9c9
2 changed files with 76 additions and 23 deletions

View File

@@ -292,6 +292,7 @@ describe("google transport stream", () => {
});
afterEach(() => {
vi.useRealTimers();
vi.unstubAllEnvs();
});
@@ -767,6 +768,29 @@ describe("google transport stream", () => {
expect(tokenFetchMock).not.toHaveBeenCalled();
});
it("does not cache google-auth ADC tokens when fallback expiry would exceed Date range", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-expiry-"));
vi.useFakeTimers();
vi.setSystemTime(new Date(8_640_000_000_000_000));
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");
vi.stubEnv("HOME", path.join(tempDir, "home"));
vi.stubEnv("APPDATA", "");
googleAuthGetAccessTokenMock
.mockResolvedValueOnce("ya29.first-token")
.mockResolvedValueOnce("ya29.second-token");
const tokenFetchMock = vi.fn();
await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
Authorization: "Bearer ya29.first-token",
});
await expect(resolveGoogleVertexAuthorizedUserHeaders(tokenFetchMock)).resolves.toEqual({
Authorization: "Bearer ya29.second-token",
});
expect(googleAuthGetAccessTokenMock).toHaveBeenCalledTimes(2);
expect(tokenFetchMock).not.toHaveBeenCalled();
});
it("uses google-auth-library bearer auth for Google Vertex credential marker requests", async () => {
const tempDir = await mkdtemp(path.join(os.tmpdir(), "openclaw-google-vertex-authlib-stream-"));
vi.stubEnv("GOOGLE_APPLICATION_CREDENTIALS", "");

View File

@@ -2,7 +2,11 @@ import { existsSync, readFileSync } from "node:fs";
import { readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveExpiresAtMsFromDurationSeconds } from "openclaw/plugin-sdk/number-runtime";
import {
asDateTimestampMs,
resolveExpiresAtMsFromDurationMs,
resolveExpiresAtMsFromDurationSeconds,
} from "openclaw/plugin-sdk/number-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
type GoogleAuthorizedUserCredentials = {
@@ -32,6 +36,7 @@ const GOOGLE_VERTEX_OAUTH_SCOPE = "https://www.googleapis.com/auth/cloud-platfor
// leaves the gateway.
const GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS = 60_000;
const GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS = 3600;
const GOOGLE_VERTEX_AUTHLIB_TOKEN_CACHE_MS = 5 * 60_000;
let cachedGoogleVertexAuthorizedUserToken: GoogleVertexAuthorizedUserToken | undefined;
let cachedGoogleAuthClient:
@@ -43,18 +48,36 @@ let cachedGoogleAuthClient:
| undefined;
let cachedGoogleVertexAdcToken: GoogleVertexAdcToken | undefined;
function resolveAuthorizedUserTokenExpiresAtMs(value: unknown, nowMs: number): number {
if (typeof value === "number" && Number.isFinite(value)) {
return (
resolveExpiresAtMsFromDurationSeconds(Math.max(1, value), { nowMs }) ??
nowMs - GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
);
function isGoogleVertexTokenFresh(expiresAtMsRaw: number, nowRaw = Date.now()): boolean {
const expiresAtMs = asDateTimestampMs(expiresAtMsRaw);
const nowMs = asDateTimestampMs(nowRaw);
if (expiresAtMs === undefined || nowMs === undefined) {
return false;
}
return (
resolveExpiresAtMsFromDurationSeconds(GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS, {
nowMs,
}) ?? nowMs - GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
const minFreshExpiresAtMs = resolveExpiresAtMsFromDurationMs(
GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS,
{ nowMs },
);
return minFreshExpiresAtMs !== undefined && expiresAtMs > minFreshExpiresAtMs;
}
function resolveAuthorizedUserTokenExpiresAtMs(value: unknown, nowRaw: number): number | undefined {
const nowMs = asDateTimestampMs(nowRaw);
if (nowMs === undefined) {
return undefined;
}
const lifetimeSeconds =
typeof value === "number" && Number.isFinite(value)
? Math.max(1, value)
: GOOGLE_VERTEX_DEFAULT_TOKEN_LIFETIME_SECONDS;
return resolveExpiresAtMsFromDurationSeconds(lifetimeSeconds, { nowMs }) ?? nowMs;
}
function resolveGoogleAuthLibraryTokenExpiresAtMs(nowRaw = Date.now()): number | undefined {
const nowMs = asDateTimestampMs(nowRaw);
return nowMs === undefined
? undefined
: resolveExpiresAtMsFromDurationMs(GOOGLE_VERTEX_AUTHLIB_TOKEN_CACHE_MS, { nowMs });
}
export function resetGoogleVertexAuthorizedUserTokenCacheForTest(): void {
@@ -177,7 +200,7 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
if (
cached?.credentialsPath === params.credentialsPath &&
cached.refreshToken === refreshToken &&
cached.expiresAtMs - Date.now() > GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS
isGoogleVertexTokenFresh(cached.expiresAtMs)
) {
return cached.token;
}
@@ -208,12 +231,15 @@ async function refreshGoogleVertexAuthorizedUserAccessToken(params: {
throw new Error("Google Vertex ADC token refresh response did not include an access_token.");
}
const nowMs = Date.now();
cachedGoogleVertexAuthorizedUserToken = {
token,
expiresAtMs: resolveAuthorizedUserTokenExpiresAtMs(payload?.expires_in, nowMs),
credentialsPath: params.credentialsPath,
refreshToken,
};
const expiresAtMs = resolveAuthorizedUserTokenExpiresAtMs(payload?.expires_in, nowMs);
if (expiresAtMs !== undefined) {
cachedGoogleVertexAuthorizedUserToken = {
token,
expiresAtMs,
credentialsPath: params.credentialsPath,
refreshToken,
};
}
return token;
}
@@ -238,7 +264,7 @@ async function resolveGoogleVertexAccessTokenViaGoogleAuth(): Promise<string> {
const auth = await cachedGoogleAuthClient.promise;
const cached = cachedGoogleVertexAdcToken;
if (cached && cached.expiresAtMs - Date.now() > GOOGLE_VERTEX_TOKEN_EXPIRY_BUFFER_MS) {
if (cached && isGoogleVertexTokenFresh(cached.expiresAtMs)) {
return cached.token;
}
@@ -255,10 +281,13 @@ async function resolveGoogleVertexAccessTokenViaGoogleAuth(): Promise<string> {
// `getAccessToken()` return type, so we cache for a conservative 5 minutes.
// The library itself already refreshes well before its own internal expiry,
// so this cache is mainly to avoid hot-loop calls into the auth client.
cachedGoogleVertexAdcToken = {
token: normalized,
expiresAtMs: Date.now() + 5 * 60_000,
};
const expiresAtMs = resolveGoogleAuthLibraryTokenExpiresAtMs();
if (expiresAtMs !== undefined) {
cachedGoogleVertexAdcToken = {
token: normalized,
expiresAtMs,
};
}
return normalized;
}