From e5248789986c5df234629a818969349605b0e65e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 4 May 2026 08:40:23 +0100 Subject: [PATCH] fix(googlechat): normalize auth response headers --- CHANGELOG.md | 1 + .../googlechat/src/google-auth.runtime.test.ts | 17 +++++++++++++++++ .../googlechat/src/google-auth.runtime.ts | 16 ++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 872d271cffa..44c516e7a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -205,6 +205,7 @@ Docs: https://docs.openclaw.ai - Feishu: accept and honor `channels.feishu.blockStreaming` at the top level and per account, while keeping the legacy default off so Feishu cards no longer reject documented config or silently drop block replies. Fixes #75555. Thanks @vincentkoc. - Gateway/update: avoid `launchctl kickstart -k` immediately after fresh macOS update bootstraps, and unlink dangling global plugin-runtime symlinks during packaged postinstall and `doctor --fix` so upgrades no longer SIGTERM the newly booted Gateway or leave bundled plugin imports pointed at pruned `plugin-runtime-deps` trees. Completes #76261 and fixes #76466. (#76929) - 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. +- Google Chat: normalize Google auth certificate response headers before google-auth-library reads cache-control, so inbound webhook auth no longer rejects with `res?.headers.get is not a function`. Fixes #76880. Thanks @donbowman. - Doctor/plugins: reset stale `plugins.slots.memory` and `plugins.slots.contextEngine` references during `doctor --fix`, so cleanup of missing plugin config does not leave unrecoverable slot owners behind. Fixes #76550 and #76551. Thanks @vincentkoc. - 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. diff --git a/extensions/googlechat/src/google-auth.runtime.test.ts b/extensions/googlechat/src/google-auth.runtime.test.ts index cb072ce96f7..1a583af9496 100644 --- a/extensions/googlechat/src/google-auth.runtime.test.ts +++ b/extensions/googlechat/src/google-auth.runtime.test.ts @@ -348,6 +348,9 @@ describe("googlechat google auth runtime", () => { expect(transport.interceptors.request.add).toHaveBeenCalledWith({ resolved: expect.any(Function), }); + expect(transport.interceptors.response.add).toHaveBeenCalledWith({ + resolved: expect.any(Function), + }); expect("window" in globalThis).toBe(false); } finally { if (originalWindowDescriptor) { @@ -369,6 +372,20 @@ describe("googlechat google auth runtime", () => { expect(normalized.headers.get("x-test")).toBe("1"); }); + it("normalizes Google auth response headers before upstream cache-control reads", () => { + const response = { + data: {}, + headers: { + "cache-control": "public, max-age=3600", + }, + }; + + const normalized = __testing.normalizeGoogleAuthResponseHeaders(response); + + expect(normalized.headers).toBeInstanceOf(Headers); + expect(normalized.headers.get("cache-control")).toBe("public, max-age=3600"); + }); + 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 d7c621442c8..171526884fb 100644 --- a/extensions/googlechat/src/google-auth.runtime.ts +++ b/extensions/googlechat/src/google-auth.runtime.ts @@ -23,6 +23,9 @@ type GoogleAuthTransport = InstanceType; type GoogleAuthRequestWithUnknownHeaders = RequestInit & { headers?: unknown; }; +type GoogleAuthResponseWithUnknownHeaders = { + headers?: unknown; +}; type GuardedGoogleAuthRequestInit = RequestInit & { agent?: unknown; cert?: unknown; @@ -79,12 +82,24 @@ function normalizeGoogleAuthPreparedRequestHeaders( + response: T, +): T & { headers: Headers } { + if (!(response.headers instanceof Headers)) { + response.headers = new Headers(response.headers as HeadersInit | undefined); + } + return response as T & { headers: Headers }; +} + function installGoogleAuthHeaderCompatibilityInterceptor( transport: GoogleAuthTransport, ): GoogleAuthTransport { transport.interceptors.request.add({ resolved: async (config) => normalizeGoogleAuthPreparedRequestHeaders(config), }); + transport.interceptors.response.add({ + resolved: async (response) => normalizeGoogleAuthResponseHeaders(response), + }); return transport; } @@ -558,6 +573,7 @@ export const __testing = { googleAuthTransportPromise = null; }, normalizeGoogleAuthPreparedRequestHeaders, + normalizeGoogleAuthResponseHeaders, resolveGoogleAuthEnvProxyUrl, validateGoogleChatServiceAccountCredentials, };