mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:40:44 +00:00
fix(googlechat): normalize auth transport headers
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user