mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 19:24:05 +00:00
fix(google): bound vertex adc token cache expiry
This commit is contained in:
@@ -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", "");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user