fix(googlechat): normalize auth transport headers

This commit is contained in:
Vincent Koc
2026-05-03 11:20:54 -07:00
parent 579cc23ce0
commit 01e2755dc3
3 changed files with 62 additions and 6 deletions

View File

@@ -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.

View File

@@ -8,9 +8,24 @@ const mocks = vi.hoisted(() => ({
hostnameAllowlist: hosts,
})),
fetchWithSsrFGuard: vi.fn(),
gaxiosCtor: vi.fn(function MockGaxios(this: { defaults: Record<string, unknown> }, defaults) {
this.defaults = defaults as Record<string, unknown>;
}),
gaxiosCtor: vi.fn(
function MockGaxios(
this: {
defaults: Record<string, unknown>;
interceptors: {
request: { add: ReturnType<typeof vi.fn> };
response: { add: ReturnType<typeof vi.fn> };
};
},
defaults,
) {
this.defaults = defaults as Record<string, unknown>;
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({

View File

@@ -20,6 +20,9 @@ type GoogleAuthRuntime = {
OAuth2Client: GoogleAuthModule["OAuth2Client"];
};
type GoogleAuthTransport = InstanceType<GaxiosModule["Gaxios"]>;
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<GoogleAuthRuntime> | null = null;
let googleAuthTransportPromise: Promise<GoogleAuthTransport> | null = null;
function normalizeGoogleAuthPreparedRequestHeaders<T extends GoogleAuthRequestWithUnknownHeaders>(
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<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
@@ -504,9 +525,11 @@ export async function getGoogleAuthTransport(): Promise<GoogleAuthTransport> {
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,
};