@kodelyth/googlechat 2026.5.42 → 2026.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/klaw.plugin.json +967 -2
- package/package.json +18 -6
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/config-api.ts +0 -2
- package/contract-api.ts +0 -5
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/accounts.ts +0 -181
- package/src/actions.test.ts +0 -289
- package/src/actions.ts +0 -227
- package/src/api.ts +0 -316
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -32
- package/src/auth.ts +0 -218
- package/src/channel-config.test.ts +0 -39
- package/src/channel.adapters.ts +0 -340
- package/src/channel.deps.runtime.ts +0 -29
- package/src/channel.runtime.ts +0 -17
- package/src/channel.setup.ts +0 -98
- package/src/channel.test.ts +0 -784
- package/src/channel.ts +0 -277
- package/src/config-schema.test.ts +0 -31
- package/src/config-schema.ts +0 -3
- package/src/doctor-contract.test.ts +0 -75
- package/src/doctor-contract.ts +0 -182
- package/src/doctor.ts +0 -57
- package/src/gateway.ts +0 -63
- package/src/google-auth.runtime.test.ts +0 -543
- package/src/google-auth.runtime.ts +0 -568
- package/src/group-policy.ts +0 -17
- package/src/monitor-access.test.ts +0 -491
- package/src/monitor-access.ts +0 -465
- package/src/monitor-durable.test.ts +0 -39
- package/src/monitor-durable.ts +0 -23
- package/src/monitor-reply-delivery.ts +0 -156
- package/src/monitor-routing.ts +0 -65
- package/src/monitor-types.ts +0 -33
- package/src/monitor-webhook.test.ts +0 -587
- package/src/monitor-webhook.ts +0 -303
- package/src/monitor.reply-delivery.test.ts +0 -144
- package/src/monitor.test.ts +0 -159
- package/src/monitor.ts +0 -527
- package/src/monitor.webhook-routing.test.ts +0 -257
- package/src/runtime.ts +0 -9
- package/src/secret-contract.test.ts +0 -60
- package/src/secret-contract.ts +0 -161
- package/src/setup-core.ts +0 -40
- package/src/setup-surface.ts +0 -243
- package/src/setup.test.ts +0 -619
- package/src/targets.test.ts +0 -453
- package/src/targets.ts +0 -66
- package/src/types.config.ts +0 -3
- package/src/types.ts +0 -73
- package/test-api.ts +0 -2
- package/tsconfig.json +0 -16
|
@@ -1,543 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
const mocks = vi.hoisted(() => ({
|
|
7
|
-
buildHostnameAllowlistPolicyFromSuffixAllowlist: vi.fn((hosts: string[]) => ({
|
|
8
|
-
hostnameAllowlist: hosts,
|
|
9
|
-
})),
|
|
10
|
-
fetchWithSsrFGuard: vi.fn(),
|
|
11
|
-
gaxiosCtor: vi.fn(
|
|
12
|
-
function MockGaxios(
|
|
13
|
-
this: {
|
|
14
|
-
defaults: Record<string, unknown>;
|
|
15
|
-
interceptors: {
|
|
16
|
-
request: { add: ReturnType<typeof vi.fn> };
|
|
17
|
-
response: { add: ReturnType<typeof vi.fn> };
|
|
18
|
-
};
|
|
19
|
-
},
|
|
20
|
-
defaults,
|
|
21
|
-
) {
|
|
22
|
-
this.defaults = defaults as Record<string, unknown>;
|
|
23
|
-
this.interceptors = {
|
|
24
|
-
request: { add: vi.fn() },
|
|
25
|
-
response: { add: vi.fn() },
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
),
|
|
29
|
-
}));
|
|
30
|
-
|
|
31
|
-
vi.mock("klaw/plugin-sdk/ssrf-runtime", () => ({
|
|
32
|
-
buildHostnameAllowlistPolicyFromSuffixAllowlist:
|
|
33
|
-
mocks.buildHostnameAllowlistPolicyFromSuffixAllowlist,
|
|
34
|
-
fetchWithSsrFGuard: mocks.fetchWithSsrFGuard,
|
|
35
|
-
}));
|
|
36
|
-
|
|
37
|
-
vi.mock("gaxios", () => ({
|
|
38
|
-
Gaxios: mocks.gaxiosCtor,
|
|
39
|
-
}));
|
|
40
|
-
|
|
41
|
-
let testing: typeof import("./google-auth.runtime.js").testing;
|
|
42
|
-
let createGoogleAuthFetch: typeof import("./google-auth.runtime.js").createGoogleAuthFetch;
|
|
43
|
-
let getGoogleAuthTransport: typeof import("./google-auth.runtime.js").getGoogleAuthTransport;
|
|
44
|
-
let resolveValidatedGoogleChatCredentials: typeof import("./google-auth.runtime.js").resolveValidatedGoogleChatCredentials;
|
|
45
|
-
|
|
46
|
-
beforeAll(async () => {
|
|
47
|
-
({
|
|
48
|
-
testing,
|
|
49
|
-
createGoogleAuthFetch,
|
|
50
|
-
getGoogleAuthTransport,
|
|
51
|
-
resolveValidatedGoogleChatCredentials,
|
|
52
|
-
} = await import("./google-auth.runtime.js"));
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
beforeEach(() => {
|
|
56
|
-
testing.resetGoogleAuthRuntimeForTests();
|
|
57
|
-
mocks.buildHostnameAllowlistPolicyFromSuffixAllowlist.mockClear();
|
|
58
|
-
mocks.fetchWithSsrFGuard.mockReset();
|
|
59
|
-
mocks.gaxiosCtor.mockClear();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
afterEach(() => {
|
|
63
|
-
vi.restoreAllMocks();
|
|
64
|
-
vi.unstubAllGlobals();
|
|
65
|
-
vi.unstubAllEnvs();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterAll(() => {
|
|
69
|
-
vi.doUnmock("klaw/plugin-sdk/ssrf-runtime");
|
|
70
|
-
vi.doUnmock("gaxios");
|
|
71
|
-
vi.resetModules();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
function mockCallArg(mock: ReturnType<typeof vi.fn>, callIndex = 0, argIndex = 0): unknown {
|
|
75
|
-
const call = mock.mock.calls[callIndex];
|
|
76
|
-
if (!call) {
|
|
77
|
-
throw new Error(`Expected mock call ${callIndex}`);
|
|
78
|
-
}
|
|
79
|
-
return call[argIndex];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
describe("googlechat google auth runtime", () => {
|
|
83
|
-
it("routes Google auth fetches through the SSRF guard and preserves explicit proxy mTLS", async () => {
|
|
84
|
-
const release = vi.fn();
|
|
85
|
-
const injectedFetch = vi.fn(globalThis.fetch);
|
|
86
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
87
|
-
response: new Response("ok", { status: 200 }),
|
|
88
|
-
release,
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
const guardedFetch = createGoogleAuthFetch(injectedFetch);
|
|
92
|
-
const response = await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
93
|
-
agent: { proxy: new URL("http://proxy.example:8080") },
|
|
94
|
-
cert: "CLIENT_CERT",
|
|
95
|
-
headers: { "content-type": "application/json" },
|
|
96
|
-
key: "CLIENT_KEY",
|
|
97
|
-
method: "POST",
|
|
98
|
-
proxy: "http://proxy.example:8080",
|
|
99
|
-
} as RequestInit);
|
|
100
|
-
|
|
101
|
-
expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
|
|
102
|
-
auditContext: "googlechat.auth.google-auth",
|
|
103
|
-
dispatcherPolicy: {
|
|
104
|
-
allowPrivateProxy: true,
|
|
105
|
-
mode: "explicit-proxy",
|
|
106
|
-
proxyTls: {
|
|
107
|
-
cert: "CLIENT_CERT",
|
|
108
|
-
key: "CLIENT_KEY",
|
|
109
|
-
},
|
|
110
|
-
proxyUrl: "http://proxy.example:8080",
|
|
111
|
-
},
|
|
112
|
-
fetchImpl: injectedFetch,
|
|
113
|
-
init: {
|
|
114
|
-
headers: { "content-type": "application/json" },
|
|
115
|
-
method: "POST",
|
|
116
|
-
},
|
|
117
|
-
policy: {
|
|
118
|
-
hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
|
|
119
|
-
},
|
|
120
|
-
url: "https://oauth2.googleapis.com/token",
|
|
121
|
-
});
|
|
122
|
-
await expect(response.text()).resolves.toBe("ok");
|
|
123
|
-
expect(release).toHaveBeenCalledOnce();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("lets the guard resolve the ambient runtime fetch when no override is injected", async () => {
|
|
127
|
-
const release = vi.fn();
|
|
128
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
129
|
-
response: new Response("ok", { status: 200 }),
|
|
130
|
-
release,
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
134
|
-
await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
135
|
-
method: "POST",
|
|
136
|
-
} as RequestInit);
|
|
137
|
-
|
|
138
|
-
expect(mockCallArg(mocks.fetchWithSsrFGuard)).not.toHaveProperty("fetchImpl");
|
|
139
|
-
expect(release).toHaveBeenCalledOnce();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("keeps using the guard-selected runtime fetch even if global fetch changes later", async () => {
|
|
143
|
-
const release = vi.fn();
|
|
144
|
-
const originalFetch = globalThis.fetch;
|
|
145
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
146
|
-
response: new Response("ok", { status: 200 }),
|
|
147
|
-
release,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
151
|
-
(globalThis as Record<string, unknown>).fetch = vi.fn(async () => new Response("patched"));
|
|
152
|
-
|
|
153
|
-
try {
|
|
154
|
-
await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
155
|
-
method: "POST",
|
|
156
|
-
} as RequestInit);
|
|
157
|
-
} finally {
|
|
158
|
-
(globalThis as Record<string, unknown>).fetch = originalFetch;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
expect(mockCallArg(mocks.fetchWithSsrFGuard)).not.toHaveProperty("fetchImpl");
|
|
162
|
-
expect(release).toHaveBeenCalledOnce();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it("bypasses explicit proxy when noProxy excludes the Google auth host", async () => {
|
|
166
|
-
const release = vi.fn();
|
|
167
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
168
|
-
response: new Response("ok", { status: 200 }),
|
|
169
|
-
release,
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
173
|
-
const response = await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
174
|
-
cert: "CLIENT_CERT",
|
|
175
|
-
key: "CLIENT_KEY",
|
|
176
|
-
method: "POST",
|
|
177
|
-
noProxy: ["oauth2.googleapis.com"],
|
|
178
|
-
proxy: "http://proxy.example:8080",
|
|
179
|
-
} as RequestInit);
|
|
180
|
-
|
|
181
|
-
expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
|
|
182
|
-
auditContext: "googlechat.auth.google-auth",
|
|
183
|
-
dispatcherPolicy: {
|
|
184
|
-
connect: {
|
|
185
|
-
cert: "CLIENT_CERT",
|
|
186
|
-
key: "CLIENT_KEY",
|
|
187
|
-
},
|
|
188
|
-
mode: "direct",
|
|
189
|
-
},
|
|
190
|
-
init: {
|
|
191
|
-
method: "POST",
|
|
192
|
-
},
|
|
193
|
-
policy: {
|
|
194
|
-
hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
|
|
195
|
-
},
|
|
196
|
-
url: "https://oauth2.googleapis.com/token",
|
|
197
|
-
});
|
|
198
|
-
await expect(response.text()).resolves.toBe("ok");
|
|
199
|
-
expect(release).toHaveBeenCalledOnce();
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it("preserves env-proxy transport when HTTPS proxy is configured", async () => {
|
|
203
|
-
const release = vi.fn();
|
|
204
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
205
|
-
response: new Response("ok", { status: 200 }),
|
|
206
|
-
release,
|
|
207
|
-
});
|
|
208
|
-
vi.stubEnv("HTTPS_PROXY", "http://env-proxy.example:8080");
|
|
209
|
-
vi.stubEnv("https_proxy", "http://lower-proxy.example:8080");
|
|
210
|
-
|
|
211
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
212
|
-
const response = await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
213
|
-
cert: "CLIENT_CERT",
|
|
214
|
-
key: "CLIENT_KEY",
|
|
215
|
-
method: "POST",
|
|
216
|
-
} as RequestInit);
|
|
217
|
-
|
|
218
|
-
expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
|
|
219
|
-
auditContext: "googlechat.auth.google-auth",
|
|
220
|
-
dispatcherPolicy: {
|
|
221
|
-
mode: "env-proxy",
|
|
222
|
-
proxyTls: {
|
|
223
|
-
cert: "CLIENT_CERT",
|
|
224
|
-
key: "CLIENT_KEY",
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
init: {
|
|
228
|
-
method: "POST",
|
|
229
|
-
},
|
|
230
|
-
policy: {
|
|
231
|
-
hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
|
|
232
|
-
},
|
|
233
|
-
url: "https://oauth2.googleapis.com/token",
|
|
234
|
-
});
|
|
235
|
-
await expect(response.text()).resolves.toBe("ok");
|
|
236
|
-
expect(release).toHaveBeenCalledOnce();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("matches gaxios proxy env precedence for Google auth requests", () => {
|
|
240
|
-
vi.stubEnv("HTTP_PROXY", "http://upper-http-proxy.example:8080");
|
|
241
|
-
vi.stubEnv("http_proxy", "http://lower-http-proxy.example:8080");
|
|
242
|
-
vi.stubEnv("HTTPS_PROXY", "http://upper-https-proxy.example:8080");
|
|
243
|
-
vi.stubEnv("https_proxy", "http://lower-https-proxy.example:8080");
|
|
244
|
-
|
|
245
|
-
expect(testing.resolveGoogleAuthEnvProxyUrl("https")).toBe(
|
|
246
|
-
"http://upper-https-proxy.example:8080",
|
|
247
|
-
);
|
|
248
|
-
expect(testing.resolveGoogleAuthEnvProxyUrl("http")).toBe(
|
|
249
|
-
"http://upper-http-proxy.example:8080",
|
|
250
|
-
);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("releases guarded auth fetch resources even when callers do not consume the body", async () => {
|
|
254
|
-
const release = vi.fn();
|
|
255
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
256
|
-
response: new Response("ok", { status: 200 }),
|
|
257
|
-
release,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
261
|
-
const response = await guardedFetch("https://oauth2.googleapis.com/token", {
|
|
262
|
-
method: "POST",
|
|
263
|
-
} as RequestInit);
|
|
264
|
-
|
|
265
|
-
expect(release).toHaveBeenCalledOnce();
|
|
266
|
-
await expect(response.text()).resolves.toBe("ok");
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("rejects oversized guarded auth responses before buffering them into memory", async () => {
|
|
270
|
-
const release = vi.fn();
|
|
271
|
-
let chunkIndex = 0;
|
|
272
|
-
const chunks = [new Uint8Array(700 * 1024), new Uint8Array(400 * 1024)];
|
|
273
|
-
const body = new ReadableStream<Uint8Array>({
|
|
274
|
-
pull(controller) {
|
|
275
|
-
if (chunkIndex < chunks.length) {
|
|
276
|
-
controller.enqueue(chunks[chunkIndex++]);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
controller.close();
|
|
280
|
-
},
|
|
281
|
-
});
|
|
282
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
283
|
-
response: new Response(body, { status: 200 }),
|
|
284
|
-
release,
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
288
|
-
|
|
289
|
-
await expect(
|
|
290
|
-
guardedFetch("https://oauth2.googleapis.com/token", {
|
|
291
|
-
method: "POST",
|
|
292
|
-
} as RequestInit),
|
|
293
|
-
).rejects.toThrow("Google auth response exceeds 1048576 bytes.");
|
|
294
|
-
expect(release).toHaveBeenCalledOnce();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("rejects non-stream guarded auth responses instead of buffering them unbounded", async () => {
|
|
298
|
-
const release = vi.fn();
|
|
299
|
-
const arrayBuffer = vi.fn(async () => new ArrayBuffer(16));
|
|
300
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
301
|
-
response: {
|
|
302
|
-
arrayBuffer,
|
|
303
|
-
body: null,
|
|
304
|
-
headers: new Headers(),
|
|
305
|
-
status: 200,
|
|
306
|
-
statusText: "OK",
|
|
307
|
-
} as unknown as Response,
|
|
308
|
-
release,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
312
|
-
|
|
313
|
-
await expect(
|
|
314
|
-
guardedFetch("https://oauth2.googleapis.com/token", {
|
|
315
|
-
method: "POST",
|
|
316
|
-
} as RequestInit),
|
|
317
|
-
).rejects.toThrow(
|
|
318
|
-
"Google auth response body stream unavailable; refusing to buffer unbounded response.",
|
|
319
|
-
);
|
|
320
|
-
expect(arrayBuffer).not.toHaveBeenCalled();
|
|
321
|
-
expect(release).toHaveBeenCalledOnce();
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it("rejects oversized auth responses from content-length before reading the body", async () => {
|
|
325
|
-
const release = vi.fn();
|
|
326
|
-
const arrayBuffer = vi.fn(async () => new ArrayBuffer(16));
|
|
327
|
-
mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
|
|
328
|
-
response: {
|
|
329
|
-
arrayBuffer,
|
|
330
|
-
body: null,
|
|
331
|
-
headers: new Headers({
|
|
332
|
-
"content-length": String(2 * 1024 * 1024),
|
|
333
|
-
}),
|
|
334
|
-
status: 200,
|
|
335
|
-
statusText: "OK",
|
|
336
|
-
} as unknown as Response,
|
|
337
|
-
release,
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
const guardedFetch = createGoogleAuthFetch();
|
|
341
|
-
|
|
342
|
-
await expect(
|
|
343
|
-
guardedFetch("https://oauth2.googleapis.com/token", {
|
|
344
|
-
method: "POST",
|
|
345
|
-
} as RequestInit),
|
|
346
|
-
).rejects.toThrow("Google auth response exceeds 1048576 bytes.");
|
|
347
|
-
expect(arrayBuffer).not.toHaveBeenCalled();
|
|
348
|
-
expect(release).toHaveBeenCalledOnce();
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it("builds a scoped Gaxios transport without mutating global window", async () => {
|
|
352
|
-
const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
353
|
-
Reflect.deleteProperty(globalThis as object, "window");
|
|
354
|
-
try {
|
|
355
|
-
const transport = await getGoogleAuthTransport();
|
|
356
|
-
const transportDefaults = transport.defaults as { fetchImplementation?: unknown };
|
|
357
|
-
const requestInterceptorAdd = transport.interceptors.request.add as unknown as ReturnType<
|
|
358
|
-
typeof vi.fn
|
|
359
|
-
>;
|
|
360
|
-
const responseInterceptorAdd = transport.interceptors.response.add as unknown as ReturnType<
|
|
361
|
-
typeof vi.fn
|
|
362
|
-
>;
|
|
363
|
-
const requestInterceptor = mockCallArg(requestInterceptorAdd) as
|
|
364
|
-
| { resolved?: unknown }
|
|
365
|
-
| undefined;
|
|
366
|
-
const responseInterceptor = mockCallArg(responseInterceptorAdd) as
|
|
367
|
-
| { resolved?: unknown }
|
|
368
|
-
| undefined;
|
|
369
|
-
|
|
370
|
-
expect(mocks.gaxiosCtor).toHaveBeenCalledOnce();
|
|
371
|
-
expect(typeof transportDefaults.fetchImplementation).toBe("function");
|
|
372
|
-
expect(requestInterceptorAdd).toHaveBeenCalledOnce();
|
|
373
|
-
expect(typeof requestInterceptor?.resolved).toBe("function");
|
|
374
|
-
expect(responseInterceptorAdd).toHaveBeenCalledOnce();
|
|
375
|
-
expect(typeof responseInterceptor?.resolved).toBe("function");
|
|
376
|
-
expect("window" in globalThis).toBe(false);
|
|
377
|
-
} finally {
|
|
378
|
-
if (originalWindowDescriptor) {
|
|
379
|
-
Object.defineProperty(globalThis, "window", originalWindowDescriptor);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it("keeps auth transports isolated from google-auth interceptor mutations", async () => {
|
|
385
|
-
const first = await getGoogleAuthTransport();
|
|
386
|
-
const second = await getGoogleAuthTransport();
|
|
387
|
-
|
|
388
|
-
expect(first).not.toBe(second);
|
|
389
|
-
expect(mocks.gaxiosCtor).toHaveBeenCalledTimes(2);
|
|
390
|
-
expect(first.interceptors.request.add).toHaveBeenCalledOnce();
|
|
391
|
-
expect(first.interceptors.response.add).toHaveBeenCalledOnce();
|
|
392
|
-
expect(second.interceptors.request.add).toHaveBeenCalledOnce();
|
|
393
|
-
expect(second.interceptors.response.add).toHaveBeenCalledOnce();
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it("normalizes Google auth request headers before upstream interceptors run", () => {
|
|
397
|
-
const config = {
|
|
398
|
-
headers: { "x-test": "1" },
|
|
399
|
-
url: new URL("https://www.googleapis.com/oauth2/v1/certs"),
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
const normalized = testing.normalizeGoogleAuthPreparedRequestHeaders(config);
|
|
403
|
-
|
|
404
|
-
expect(normalized.headers).toBeInstanceOf(Headers);
|
|
405
|
-
expect(normalized.headers.has("x-test")).toBe(true);
|
|
406
|
-
expect(normalized.headers.get("x-test")).toBe("1");
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
it("normalizes Google auth response headers before upstream cache-control reads", () => {
|
|
410
|
-
const response = {
|
|
411
|
-
data: {},
|
|
412
|
-
headers: {
|
|
413
|
-
"cache-control": "public, max-age=3600",
|
|
414
|
-
},
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
const normalized = testing.normalizeGoogleAuthResponseHeaders(response);
|
|
418
|
-
|
|
419
|
-
expect(normalized.headers).toBeInstanceOf(Headers);
|
|
420
|
-
expect(normalized.headers.get("cache-control")).toBe("public, max-age=3600");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
it("rejects service-account credentials that override Google auth endpoints", async () => {
|
|
424
|
-
await expect(
|
|
425
|
-
resolveValidatedGoogleChatCredentials({
|
|
426
|
-
accountId: "default",
|
|
427
|
-
config: {},
|
|
428
|
-
credentialSource: "inline",
|
|
429
|
-
credentials: {
|
|
430
|
-
client_email: "bot@example.iam.gserviceaccount.com",
|
|
431
|
-
private_key: "key",
|
|
432
|
-
token_uri: "https://evil.example/token",
|
|
433
|
-
type: "service_account",
|
|
434
|
-
},
|
|
435
|
-
enabled: true,
|
|
436
|
-
}),
|
|
437
|
-
).rejects.toThrow(/token_uri/);
|
|
438
|
-
});
|
|
439
|
-
|
|
440
|
-
it("reads and validates service-account files before passing them to google-auth", async () => {
|
|
441
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "googlechat-auth-"));
|
|
442
|
-
try {
|
|
443
|
-
const credentialsPath = path.join(tempDir, "service-account.json");
|
|
444
|
-
await fs.writeFile(
|
|
445
|
-
credentialsPath,
|
|
446
|
-
JSON.stringify({
|
|
447
|
-
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
|
|
448
|
-
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
|
449
|
-
client_email: "bot@example.iam.gserviceaccount.com",
|
|
450
|
-
private_key: "key",
|
|
451
|
-
token_uri: "https://oauth2.googleapis.com/token",
|
|
452
|
-
type: "service_account",
|
|
453
|
-
universe_domain: "googleapis.com",
|
|
454
|
-
}),
|
|
455
|
-
"utf8",
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const credentials = await resolveValidatedGoogleChatCredentials({
|
|
459
|
-
accountId: "default",
|
|
460
|
-
config: {},
|
|
461
|
-
credentialSource: "file",
|
|
462
|
-
credentialsFile: credentialsPath,
|
|
463
|
-
enabled: true,
|
|
464
|
-
});
|
|
465
|
-
if (!credentials) {
|
|
466
|
-
throw new Error("expected validated credentials");
|
|
467
|
-
}
|
|
468
|
-
expect(credentials.client_email).toBe("bot@example.iam.gserviceaccount.com");
|
|
469
|
-
expect(credentials.token_uri).toBe("https://oauth2.googleapis.com/token");
|
|
470
|
-
expect(credentials.type).toBe("service_account");
|
|
471
|
-
} finally {
|
|
472
|
-
await fs.rm(tempDir, { force: true, recursive: true });
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("accepts symlinked service-account files used by secret mounts", async () => {
|
|
477
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "googlechat-auth-link-"));
|
|
478
|
-
try {
|
|
479
|
-
const credentialsPath = path.join(tempDir, "service-account.json");
|
|
480
|
-
const symlinkPath = path.join(tempDir, "service-account-link.json");
|
|
481
|
-
await fs.writeFile(
|
|
482
|
-
credentialsPath,
|
|
483
|
-
JSON.stringify({
|
|
484
|
-
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
|
|
485
|
-
auth_uri: "https://accounts.google.com/o/oauth2/auth",
|
|
486
|
-
client_email: "bot@example.iam.gserviceaccount.com",
|
|
487
|
-
private_key: "key",
|
|
488
|
-
token_uri: "https://oauth2.googleapis.com/token",
|
|
489
|
-
type: "service_account",
|
|
490
|
-
universe_domain: "googleapis.com",
|
|
491
|
-
}),
|
|
492
|
-
"utf8",
|
|
493
|
-
);
|
|
494
|
-
try {
|
|
495
|
-
await fs.symlink(credentialsPath, symlinkPath);
|
|
496
|
-
} catch (error) {
|
|
497
|
-
if ((error as NodeJS.ErrnoException).code === "EPERM") {
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
throw error;
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
const credentials = await resolveValidatedGoogleChatCredentials({
|
|
504
|
-
accountId: "default",
|
|
505
|
-
config: {},
|
|
506
|
-
credentialSource: "file",
|
|
507
|
-
credentialsFile: symlinkPath,
|
|
508
|
-
enabled: true,
|
|
509
|
-
});
|
|
510
|
-
if (!credentials) {
|
|
511
|
-
throw new Error("expected validated credentials");
|
|
512
|
-
}
|
|
513
|
-
expect(credentials.client_email).toBe("bot@example.iam.gserviceaccount.com");
|
|
514
|
-
expect(credentials.token_uri).toBe("https://oauth2.googleapis.com/token");
|
|
515
|
-
expect(credentials.type).toBe("service_account");
|
|
516
|
-
} finally {
|
|
517
|
-
await fs.rm(tempDir, { force: true, recursive: true });
|
|
518
|
-
}
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
it("does not disclose raw credential paths or OS errors when file reads fail", async () => {
|
|
522
|
-
const missingPath = path.join(os.tmpdir(), "googlechat-auth-missing", "service-account.json");
|
|
523
|
-
|
|
524
|
-
let thrown: unknown;
|
|
525
|
-
try {
|
|
526
|
-
await resolveValidatedGoogleChatCredentials({
|
|
527
|
-
accountId: "default",
|
|
528
|
-
config: {},
|
|
529
|
-
credentialSource: "file",
|
|
530
|
-
credentialsFile: missingPath,
|
|
531
|
-
enabled: true,
|
|
532
|
-
});
|
|
533
|
-
} catch (error) {
|
|
534
|
-
thrown = error;
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
expect(thrown).toBeInstanceOf(Error);
|
|
538
|
-
expect((thrown as Error).message).toBe("Failed to load Google Chat service account file.");
|
|
539
|
-
expect((thrown as Error).message).not.toMatch(
|
|
540
|
-
/ENOENT|service-account\.json|googlechat-auth-missing/,
|
|
541
|
-
);
|
|
542
|
-
});
|
|
543
|
-
});
|