@kodelyth/googlechat 2026.5.39 → 2026.5.42

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.
Files changed (91) hide show
  1. package/api.ts +3 -0
  2. package/channel-config-api.ts +1 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/config-api.ts +2 -0
  5. package/contract-api.ts +5 -0
  6. package/dist/actions-YK1wn4ed.js +160 -0
  7. package/dist/api-BkZX4VNX.js +633 -0
  8. package/dist/api.js +3 -0
  9. package/dist/channel-DFZdjXD6.js +584 -0
  10. package/dist/channel-config-api.js +6 -0
  11. package/dist/channel-plugin-api.js +2 -0
  12. package/dist/channel.runtime-en3RNg9S.js +998 -0
  13. package/dist/contract-api.js +3 -0
  14. package/dist/doctor-contract-8SF6XoKj.js +151 -0
  15. package/dist/doctor-contract-api.js +2 -0
  16. package/dist/index.js +22 -0
  17. package/dist/runtime-api-DUH2Cg-0.js +29 -0
  18. package/dist/runtime-api.js +2 -0
  19. package/dist/secret-contract-DWX4ikgT.js +99 -0
  20. package/dist/secret-contract-api.js +2 -0
  21. package/dist/setup-entry.js +15 -0
  22. package/dist/setup-plugin-api.js +75 -0
  23. package/dist/setup-surface-B3Fa7XRx.js +321 -0
  24. package/dist/test-api.js +3 -0
  25. package/doctor-contract-api.ts +1 -0
  26. package/index.ts +20 -0
  27. package/klaw.plugin.json +2 -967
  28. package/package.json +4 -4
  29. package/runtime-api.ts +55 -0
  30. package/secret-contract-api.ts +5 -0
  31. package/setup-entry.ts +13 -0
  32. package/setup-plugin-api.ts +3 -0
  33. package/src/accounts.ts +181 -0
  34. package/src/actions.test.ts +289 -0
  35. package/src/actions.ts +227 -0
  36. package/src/api.ts +316 -0
  37. package/src/approval-auth.test.ts +24 -0
  38. package/src/approval-auth.ts +32 -0
  39. package/src/auth.ts +218 -0
  40. package/src/channel-config.test.ts +39 -0
  41. package/src/channel.adapters.ts +340 -0
  42. package/src/channel.deps.runtime.ts +29 -0
  43. package/src/channel.runtime.ts +17 -0
  44. package/src/channel.setup.ts +98 -0
  45. package/src/channel.test.ts +784 -0
  46. package/src/channel.ts +277 -0
  47. package/src/config-schema.test.ts +31 -0
  48. package/src/config-schema.ts +3 -0
  49. package/src/doctor-contract.test.ts +75 -0
  50. package/src/doctor-contract.ts +182 -0
  51. package/src/doctor.ts +57 -0
  52. package/src/gateway.ts +63 -0
  53. package/src/google-auth.runtime.test.ts +543 -0
  54. package/src/google-auth.runtime.ts +568 -0
  55. package/src/group-policy.ts +17 -0
  56. package/src/monitor-access.test.ts +491 -0
  57. package/src/monitor-access.ts +465 -0
  58. package/src/monitor-durable.test.ts +39 -0
  59. package/src/monitor-durable.ts +23 -0
  60. package/src/monitor-reply-delivery.ts +156 -0
  61. package/src/monitor-routing.ts +65 -0
  62. package/src/monitor-types.ts +33 -0
  63. package/src/monitor-webhook.test.ts +587 -0
  64. package/src/monitor-webhook.ts +303 -0
  65. package/src/monitor.reply-delivery.test.ts +144 -0
  66. package/src/monitor.test.ts +159 -0
  67. package/src/monitor.ts +527 -0
  68. package/src/monitor.webhook-routing.test.ts +257 -0
  69. package/src/runtime.ts +9 -0
  70. package/src/secret-contract.test.ts +60 -0
  71. package/src/secret-contract.ts +161 -0
  72. package/src/setup-core.ts +40 -0
  73. package/src/setup-surface.ts +243 -0
  74. package/src/setup.test.ts +619 -0
  75. package/src/targets.test.ts +453 -0
  76. package/src/targets.ts +66 -0
  77. package/src/types.config.ts +3 -0
  78. package/src/types.ts +73 -0
  79. package/test-api.ts +2 -0
  80. package/tsconfig.json +16 -0
  81. package/api.js +0 -7
  82. package/channel-config-api.js +0 -7
  83. package/channel-plugin-api.js +0 -7
  84. package/contract-api.js +0 -7
  85. package/doctor-contract-api.js +0 -7
  86. package/index.js +0 -7
  87. package/runtime-api.js +0 -7
  88. package/secret-contract-api.js +0 -7
  89. package/setup-entry.js +0 -7
  90. package/setup-plugin-api.js +0 -7
  91. package/test-api.js +0 -7
@@ -0,0 +1,491 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
2
+
3
+ const createChannelPairingController = vi.hoisted(() => vi.fn());
4
+ const isDangerousNameMatchingEnabled = vi.hoisted(() => vi.fn());
5
+ const resolveAllowlistProviderRuntimeGroupPolicy = vi.hoisted(() => vi.fn());
6
+ const resolveDefaultGroupPolicy = vi.hoisted(() => vi.fn());
7
+ const warnMissingProviderGroupPolicyFallbackOnce = vi.hoisted(() => vi.fn());
8
+ const sendGoogleChatMessage = vi.hoisted(() => vi.fn());
9
+
10
+ vi.mock("../runtime-api.js", () => ({
11
+ GROUP_POLICY_BLOCKED_LABEL: { space: "space" },
12
+ createChannelPairingController,
13
+ isDangerousNameMatchingEnabled,
14
+ resolveAllowlistProviderRuntimeGroupPolicy,
15
+ resolveDefaultGroupPolicy,
16
+ warnMissingProviderGroupPolicyFallbackOnce,
17
+ }));
18
+
19
+ vi.mock("./api.js", () => ({
20
+ sendGoogleChatMessage,
21
+ }));
22
+
23
+ function createCore() {
24
+ return {
25
+ channel: {
26
+ commands: {
27
+ shouldComputeCommandAuthorized: vi.fn(() => false),
28
+ resolveCommandAuthorizedFromAuthorizers: vi.fn(() => false),
29
+ shouldHandleTextCommands: vi.fn(() => false),
30
+ isControlCommandMessage: vi.fn(() => false),
31
+ },
32
+ text: {
33
+ hasControlCommand: vi.fn(() => false),
34
+ },
35
+ },
36
+ };
37
+ }
38
+
39
+ function primeCommonDefaults() {
40
+ isDangerousNameMatchingEnabled.mockReturnValue(false);
41
+ resolveDefaultGroupPolicy.mockReturnValue("allowlist");
42
+ resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
43
+ groupPolicy: "allowlist",
44
+ providerMissingFallbackApplied: false,
45
+ });
46
+ warnMissingProviderGroupPolicyFallbackOnce.mockReturnValue(undefined);
47
+ }
48
+
49
+ const baseAccessConfig = {
50
+ channels: { googlechat: {} },
51
+ commands: { useAccessGroups: true },
52
+ } as const;
53
+
54
+ const defaultSender = {
55
+ senderId: "users/alice",
56
+ senderName: "Alice",
57
+ senderEmail: "alice@example.com",
58
+ } as const;
59
+
60
+ let applyGoogleChatInboundAccessPolicy: typeof import("./monitor-access.js").applyGoogleChatInboundAccessPolicy;
61
+
62
+ function allowInboundGroupTraffic() {
63
+ createChannelPairingController.mockReturnValue({
64
+ readAllowFromStore: vi.fn(async () => []),
65
+ issueChallenge: vi.fn(),
66
+ });
67
+ }
68
+
69
+ async function applyInboundAccessPolicy(
70
+ overrides: Partial<Parameters<typeof applyGoogleChatInboundAccessPolicy>[0]>,
71
+ ) {
72
+ return applyGoogleChatInboundAccessPolicy({
73
+ account: {
74
+ accountId: "default",
75
+ config: {},
76
+ } as never,
77
+ config: baseAccessConfig as never,
78
+ core: createCore() as never,
79
+ space: { name: "spaces/AAA", displayName: "Team Room" } as never,
80
+ message: { annotations: [] } as never,
81
+ isGroup: true,
82
+ rawBody: "hello team",
83
+ logVerbose: vi.fn(),
84
+ ...defaultSender,
85
+ ...overrides,
86
+ } as never);
87
+ }
88
+
89
+ describe("googlechat inbound access policy", () => {
90
+ beforeAll(async () => {
91
+ ({ applyGoogleChatInboundAccessPolicy } = await import("./monitor-access.js"));
92
+ });
93
+
94
+ afterAll(() => {
95
+ vi.doUnmock("../runtime-api.js");
96
+ vi.doUnmock("./api.js");
97
+ vi.resetModules();
98
+ });
99
+
100
+ it.each([
101
+ {
102
+ name: "blocks raw email entries when dangerous name matching is disabled",
103
+ allowNameMatching: false,
104
+ allowFrom: ["jane@example.com"],
105
+ senderId: "users/123",
106
+ ok: false,
107
+ },
108
+ {
109
+ name: "matches raw email entries when dangerous name matching is enabled",
110
+ allowNameMatching: true,
111
+ allowFrom: ["jane@example.com"],
112
+ senderId: "users/123",
113
+ ok: true,
114
+ },
115
+ {
116
+ name: "does not treat users/<email> entries as email allowlist entries",
117
+ allowNameMatching: true,
118
+ allowFrom: ["users/jane@example.com"],
119
+ senderId: "users/123",
120
+ ok: false,
121
+ },
122
+ {
123
+ name: "matches user id entries",
124
+ allowNameMatching: false,
125
+ allowFrom: ["users/abc"],
126
+ senderId: "users/abc",
127
+ ok: true,
128
+ },
129
+ ])("$name", async ({ allowNameMatching, allowFrom, senderId, ok }) => {
130
+ primeCommonDefaults();
131
+ isDangerousNameMatchingEnabled.mockReturnValue(allowNameMatching);
132
+ createChannelPairingController.mockReturnValue({
133
+ readAllowFromStore: vi.fn(async () => []),
134
+ issueChallenge: vi.fn(),
135
+ });
136
+
137
+ const result = await applyInboundAccessPolicy({
138
+ isGroup: false,
139
+ account: {
140
+ accountId: "default",
141
+ config: {
142
+ dm: {
143
+ policy: "allowlist",
144
+ allowFrom,
145
+ },
146
+ },
147
+ } as never,
148
+ senderId,
149
+ senderEmail: "Jane@Example.com",
150
+ });
151
+ expect(result.ok).toBe(ok);
152
+ });
153
+
154
+ it("issues a pairing challenge for unauthorized DMs in pairing mode", async () => {
155
+ primeCommonDefaults();
156
+ const now = new Date("2026-05-09T06:35:00.000Z").getTime();
157
+ const issueChallenge = vi.fn(async ({ onCreated, sendPairingReply }) => {
158
+ onCreated?.();
159
+ await sendPairingReply("pairing text");
160
+ });
161
+ createChannelPairingController.mockReturnValue({
162
+ readAllowFromStore: vi.fn(async () => []),
163
+ issueChallenge,
164
+ });
165
+ sendGoogleChatMessage.mockResolvedValue({ ok: true });
166
+
167
+ const statusSink = vi.fn();
168
+ const logVerbose = vi.fn();
169
+ const account = {
170
+ accountId: "default",
171
+ config: {
172
+ dm: { policy: "pairing" },
173
+ },
174
+ };
175
+
176
+ vi.useFakeTimers();
177
+ vi.setSystemTime(now);
178
+ try {
179
+ await expect(
180
+ applyGoogleChatInboundAccessPolicy({
181
+ account: account as never,
182
+ config: {
183
+ channels: { googlechat: {} },
184
+ } as never,
185
+ core: createCore() as never,
186
+ space: { name: "spaces/AAA", displayName: "DM" } as never,
187
+ message: { annotations: [] } as never,
188
+ isGroup: false,
189
+ senderId: "users/abc",
190
+ senderName: "Alice",
191
+ senderEmail: "alice@example.com",
192
+ rawBody: "hello",
193
+ statusSink,
194
+ logVerbose,
195
+ }),
196
+ ).resolves.toEqual({ ok: false });
197
+
198
+ expect(issueChallenge).toHaveBeenCalledTimes(1);
199
+ expect(sendGoogleChatMessage).toHaveBeenCalledWith({
200
+ account,
201
+ space: "spaces/AAA",
202
+ text: "pairing text",
203
+ });
204
+ expect(statusSink).toHaveBeenCalledWith({
205
+ lastOutboundAt: now,
206
+ });
207
+ } finally {
208
+ vi.useRealTimers();
209
+ }
210
+ });
211
+
212
+ it("allows group traffic when sender and mention gates pass", async () => {
213
+ primeCommonDefaults();
214
+ allowInboundGroupTraffic();
215
+ const core = createCore();
216
+ core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true);
217
+ core.channel.commands.resolveCommandAuthorizedFromAuthorizers.mockReturnValue(true);
218
+
219
+ await expect(
220
+ applyInboundAccessPolicy({
221
+ account: {
222
+ accountId: "default",
223
+ config: {
224
+ botUser: "users/app-bot",
225
+ groups: {
226
+ "spaces/AAA": {
227
+ users: ["users/alice"],
228
+ requireMention: true,
229
+ systemPrompt: " group prompt ",
230
+ },
231
+ },
232
+ },
233
+ } as never,
234
+ core: core as never,
235
+ message: {
236
+ annotations: [
237
+ {
238
+ type: "USER_MENTION",
239
+ userMention: { user: { name: "users/app-bot" } },
240
+ },
241
+ ],
242
+ } as never,
243
+ }),
244
+ ).resolves.toEqual({
245
+ ok: true,
246
+ commandAuthorized: true,
247
+ effectiveWasMentioned: true,
248
+ groupSystemPrompt: "group prompt",
249
+ });
250
+ });
251
+
252
+ it("allows group traffic from generic message sender access groups", async () => {
253
+ primeCommonDefaults();
254
+ allowInboundGroupTraffic();
255
+
256
+ const result = await applyInboundAccessPolicy({
257
+ config: {
258
+ ...baseAccessConfig,
259
+ accessGroups: {
260
+ operators: {
261
+ type: "message.senders",
262
+ members: {
263
+ googlechat: ["users/alice"],
264
+ },
265
+ },
266
+ },
267
+ } as never,
268
+ account: {
269
+ accountId: "default",
270
+ config: {
271
+ groups: {
272
+ "spaces/AAA": {
273
+ users: ["accessGroup:operators"],
274
+ requireMention: false,
275
+ },
276
+ },
277
+ },
278
+ } as never,
279
+ });
280
+ expect(result.ok).toBe(true);
281
+ });
282
+
283
+ it("expands generic message sender access groups before DM access checks", async () => {
284
+ primeCommonDefaults();
285
+ const readAllowFromStore = vi.fn(async () => []);
286
+ createChannelPairingController.mockReturnValue({
287
+ readAllowFromStore,
288
+ issueChallenge: vi.fn(),
289
+ });
290
+
291
+ const result = await applyInboundAccessPolicy({
292
+ isGroup: false,
293
+ config: {
294
+ ...baseAccessConfig,
295
+ accessGroups: {
296
+ operators: {
297
+ type: "message.senders",
298
+ members: {
299
+ googlechat: ["users/alice"],
300
+ },
301
+ },
302
+ },
303
+ } as never,
304
+ account: {
305
+ accountId: "default",
306
+ config: {
307
+ dm: {
308
+ policy: "allowlist",
309
+ allowFrom: ["accessGroup:operators"],
310
+ },
311
+ },
312
+ } as never,
313
+ });
314
+ expect(result.ok).toBe(true);
315
+
316
+ expect(readAllowFromStore).not.toHaveBeenCalled();
317
+ });
318
+
319
+ it("preserves allowlist group policy when a routed space has no sender allowlist", async () => {
320
+ primeCommonDefaults();
321
+ allowInboundGroupTraffic();
322
+ const logVerbose = vi.fn();
323
+
324
+ await expect(
325
+ applyInboundAccessPolicy({
326
+ account: {
327
+ accountId: "default",
328
+ config: {
329
+ dm: {
330
+ policy: "allowlist",
331
+ allowFrom: ["users/alice"],
332
+ },
333
+ groups: {
334
+ "spaces/AAA": {
335
+ enabled: true,
336
+ },
337
+ },
338
+ },
339
+ } as never,
340
+ logVerbose,
341
+ }),
342
+ ).resolves.toEqual({ ok: false });
343
+
344
+ expect(logVerbose).toHaveBeenCalledWith(
345
+ "drop group message (sender policy blocked, reason=groupPolicy=allowlist (empty allowlist), space=spaces/AAA)",
346
+ );
347
+ });
348
+
349
+ it("keeps configured space users sender-scoped when group policy is open", async () => {
350
+ primeCommonDefaults();
351
+ resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
352
+ groupPolicy: "open",
353
+ providerMissingFallbackApplied: false,
354
+ });
355
+ allowInboundGroupTraffic();
356
+ const logVerbose = vi.fn();
357
+
358
+ await expect(
359
+ applyInboundAccessPolicy({
360
+ account: {
361
+ accountId: "default",
362
+ config: {
363
+ groupPolicy: "open",
364
+ groups: {
365
+ "spaces/AAA": {
366
+ users: ["users/bob"],
367
+ requireMention: false,
368
+ },
369
+ },
370
+ },
371
+ } as never,
372
+ logVerbose,
373
+ }),
374
+ ).resolves.toEqual({ ok: false });
375
+
376
+ expect(logVerbose).toHaveBeenCalledWith("drop group message (sender not allowed, users/alice)");
377
+ });
378
+
379
+ it("drops unauthorized group control commands", async () => {
380
+ primeCommonDefaults();
381
+ allowInboundGroupTraffic();
382
+ resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
383
+ groupPolicy: "open",
384
+ providerMissingFallbackApplied: false,
385
+ });
386
+ const core = createCore();
387
+ core.channel.commands.shouldComputeCommandAuthorized.mockReturnValue(true);
388
+ core.channel.commands.isControlCommandMessage.mockReturnValue(true);
389
+ const logVerbose = vi.fn();
390
+
391
+ await expect(
392
+ applyInboundAccessPolicy({
393
+ core: core as never,
394
+ account: {
395
+ accountId: "default",
396
+ config: {
397
+ groups: {
398
+ "spaces/AAA": {
399
+ requireMention: false,
400
+ },
401
+ },
402
+ },
403
+ } as never,
404
+ rawBody: "/admin",
405
+ logVerbose,
406
+ }),
407
+ ).resolves.toEqual({ ok: false });
408
+
409
+ expect(logVerbose).toHaveBeenCalledWith("googlechat: drop control command from users/alice");
410
+ });
411
+
412
+ it("does not match group policy by mutable space displayName when the stable id differs", async () => {
413
+ primeCommonDefaults();
414
+ allowInboundGroupTraffic();
415
+ const logVerbose = vi.fn();
416
+
417
+ await expect(
418
+ applyInboundAccessPolicy({
419
+ account: {
420
+ accountId: "default",
421
+ config: {
422
+ groups: {
423
+ "Finance Ops": {
424
+ users: ["users/alice"],
425
+ requireMention: true,
426
+ systemPrompt: "finance-only prompt",
427
+ },
428
+ },
429
+ },
430
+ } as never,
431
+ core: createCore() as never,
432
+ space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
433
+ message: {
434
+ annotations: [
435
+ {
436
+ type: "USER_MENTION",
437
+ userMention: { user: { name: "users/app" } },
438
+ },
439
+ ],
440
+ } as never,
441
+ rawBody: "show quarter close status",
442
+ logVerbose,
443
+ }),
444
+ ).resolves.toEqual({ ok: false });
445
+
446
+ expect(logVerbose).toHaveBeenCalledWith(
447
+ "Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: Finance Ops",
448
+ );
449
+ expect(logVerbose).toHaveBeenCalledWith(
450
+ "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
451
+ );
452
+ });
453
+
454
+ it("fails closed instead of falling back to wildcard when a deprecated room key matches", async () => {
455
+ primeCommonDefaults();
456
+ resolveAllowlistProviderRuntimeGroupPolicy.mockReturnValue({
457
+ groupPolicy: "open",
458
+ providerMissingFallbackApplied: false,
459
+ });
460
+ allowInboundGroupTraffic();
461
+ const logVerbose = vi.fn();
462
+
463
+ await expect(
464
+ applyInboundAccessPolicy({
465
+ account: {
466
+ accountId: "default",
467
+ config: {
468
+ groupPolicy: "open",
469
+ groups: {
470
+ "*": {
471
+ users: ["users/alice"],
472
+ },
473
+ "Finance Ops": {
474
+ enabled: false,
475
+ users: ["users/bob"],
476
+ },
477
+ },
478
+ },
479
+ } as never,
480
+ core: createCore() as never,
481
+ space: { name: "spaces/BBB", displayName: "Finance Ops" } as never,
482
+ rawBody: "show quarter close status",
483
+ logVerbose,
484
+ }),
485
+ ).resolves.toEqual({ ok: false });
486
+
487
+ expect(logVerbose).toHaveBeenCalledWith(
488
+ "drop group message (deprecated mutable group key matched, space=spaces/BBB)",
489
+ );
490
+ });
491
+ });