From 01e2755dc3c74ebb2338d8ed97c4f01656e54b44 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 11:20:54 -0700 Subject: [PATCH] fix(googlechat): normalize auth transport headers --- CHANGELOG.md | 1 + .../src/google-auth.runtime.test.ts | 37 +++++++++++++++++-- .../googlechat/src/google-auth.runtime.ts | 30 +++++++++++++-- 3 files changed, 62 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f77b32b562..1a4254538bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Google Chat: normalize custom Google auth transport headers before google-auth/gaxios interceptors run, restoring webhook token verification when certificate retrieval expects Fetch `Headers`. Fixes #76742. Thanks @donbowman. - Docs/WhatsApp: merge the duplicate top-level `web` objects in the gateway channel config example so copy-pasted WhatsApp config keeps both `web.whatsapp` and reconnect settings. Fixes #76619. Thanks @WadydX. - Plugins/Anthropic: expose Claude thinking profiles from the bundled provider-policy artifact so non-runtime callers keep Opus 4.7 `adaptive`, `xhigh`, and `max` instead of downgrading to `high`. Fixes #76779. Thanks @tomascupr and @iAbhi001. - Discord/native commands: skip slash-command registration and cleanup REST calls when `channels.discord.commands.native=false`, letting low-power gateways start without waiting on disabled native-command lifecycle requests. Fixes #76202. Thanks @vincentkoc. diff --git a/extensions/googlechat/src/google-auth.runtime.test.ts b/extensions/googlechat/src/google-auth.runtime.test.ts index 905a166ec9a..cb072ce96f7 100644 --- a/extensions/googlechat/src/google-auth.runtime.test.ts +++ b/extensions/googlechat/src/google-auth.runtime.test.ts @@ -8,9 +8,24 @@ const mocks = vi.hoisted(() => ({ hostnameAllowlist: hosts, })), fetchWithSsrFGuard: vi.fn(), - gaxiosCtor: vi.fn(function MockGaxios(this: { defaults: Record }, defaults) { - this.defaults = defaults as Record; - }), + gaxiosCtor: vi.fn( + function MockGaxios( + this: { + defaults: Record; + interceptors: { + request: { add: ReturnType }; + response: { add: ReturnType }; + }; + }, + defaults, + ) { + this.defaults = defaults as Record; + this.interceptors = { + request: { add: vi.fn() }, + response: { add: vi.fn() }, + }; + }, + ), })); vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({ @@ -330,6 +345,9 @@ describe("googlechat google auth runtime", () => { fetchImplementation: expect.any(Function), }, }); + expect(transport.interceptors.request.add).toHaveBeenCalledWith({ + resolved: expect.any(Function), + }); expect("window" in globalThis).toBe(false); } finally { if (originalWindowDescriptor) { @@ -338,6 +356,19 @@ describe("googlechat google auth runtime", () => { } }); + it("normalizes Google auth request headers before upstream interceptors run", async () => { + const config = { + headers: { "x-test": "1" }, + url: new URL("https://www.googleapis.com/oauth2/v1/certs"), + }; + + const normalized = __testing.normalizeGoogleAuthPreparedRequestHeaders(config); + + expect(normalized.headers).toBeInstanceOf(Headers); + expect(normalized.headers.has("x-test")).toBe(true); + expect(normalized.headers.get("x-test")).toBe("1"); + }); + it("rejects service-account credentials that override Google auth endpoints", async () => { await expect( resolveValidatedGoogleChatCredentials({ diff --git a/extensions/googlechat/src/google-auth.runtime.ts b/extensions/googlechat/src/google-auth.runtime.ts index a78ad915d05..d7c621442c8 100644 --- a/extensions/googlechat/src/google-auth.runtime.ts +++ b/extensions/googlechat/src/google-auth.runtime.ts @@ -20,6 +20,9 @@ type GoogleAuthRuntime = { OAuth2Client: GoogleAuthModule["OAuth2Client"]; }; type GoogleAuthTransport = InstanceType; +type GoogleAuthRequestWithUnknownHeaders = RequestInit & { + headers?: unknown; +}; type GuardedGoogleAuthRequestInit = RequestInit & { agent?: unknown; cert?: unknown; @@ -67,6 +70,24 @@ const MAX_GOOGLE_CHAT_SERVICE_ACCOUNT_FILE_BYTES = 64 * 1024; let googleAuthRuntimePromise: Promise | null = null; let googleAuthTransportPromise: Promise | null = null; +function normalizeGoogleAuthPreparedRequestHeaders( + config: T, +): T & { headers: Headers } { + if (!(config.headers instanceof Headers)) { + config.headers = new Headers(config.headers as HeadersInit | undefined); + } + return config as T & { headers: Headers }; +} + +function installGoogleAuthHeaderCompatibilityInterceptor( + transport: GoogleAuthTransport, +): GoogleAuthTransport { + transport.interceptors.request.add({ + resolved: async (config) => normalizeGoogleAuthPreparedRequestHeaders(config), + }); + return transport; +} + function asNullableObjectRecord(value: unknown): Record | null { return value !== null && typeof value === "object" ? (value as Record) : null; } @@ -504,9 +525,11 @@ export async function getGoogleAuthTransport(): Promise { googleAuthTransportPromise = (async () => { try { const { Gaxios } = await loadGoogleAuthRuntime(); - return new Gaxios({ - fetchImplementation: createGoogleAuthFetch(), - }); + return installGoogleAuthHeaderCompatibilityInterceptor( + new Gaxios({ + fetchImplementation: createGoogleAuthFetch(), + }), + ); } catch (error) { googleAuthTransportPromise = null; throw error; @@ -534,6 +557,7 @@ export const __testing = { googleAuthRuntimePromise = null; googleAuthTransportPromise = null; }, + normalizeGoogleAuthPreparedRequestHeaders, resolveGoogleAuthEnvProxyUrl, validateGoogleChatServiceAccountCredentials, };