@openclaw/googlechat 2026.5.2-beta.1 → 2026.5.3-beta.1

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 (75) hide show
  1. package/dist/api-C67jv0ow.js +627 -0
  2. package/dist/api.js +3 -0
  3. package/dist/channel-DE2hroMn.js +682 -0
  4. package/dist/channel-config-api.js +5 -0
  5. package/dist/channel-plugin-api.js +2 -0
  6. package/dist/channel.runtime-CoSLc-4R.js +920 -0
  7. package/dist/contract-api.js +3 -0
  8. package/dist/doctor-contract-CG1sLToP.js +151 -0
  9. package/dist/doctor-contract-api.js +2 -0
  10. package/dist/index.js +22 -0
  11. package/dist/runtime-api-wkIdfwqY.js +30 -0
  12. package/dist/runtime-api.js +2 -0
  13. package/dist/secret-contract-DtQ_IO7J.js +99 -0
  14. package/dist/secret-contract-api.js +2 -0
  15. package/dist/setup-entry.js +15 -0
  16. package/dist/setup-plugin-api.js +75 -0
  17. package/dist/setup-surface-CofP-Gg9.js +309 -0
  18. package/dist/test-api.js +3 -0
  19. package/package.json +14 -6
  20. package/api.ts +0 -3
  21. package/channel-config-api.ts +0 -1
  22. package/channel-plugin-api.ts +0 -1
  23. package/contract-api.ts +0 -5
  24. package/doctor-contract-api.ts +0 -1
  25. package/index.ts +0 -20
  26. package/runtime-api.ts +0 -60
  27. package/secret-contract-api.ts +0 -5
  28. package/setup-entry.ts +0 -13
  29. package/setup-plugin-api.ts +0 -3
  30. package/src/accounts.ts +0 -169
  31. package/src/actions.test.ts +0 -265
  32. package/src/actions.ts +0 -227
  33. package/src/api.ts +0 -322
  34. package/src/approval-auth.test.ts +0 -24
  35. package/src/approval-auth.ts +0 -32
  36. package/src/auth.ts +0 -209
  37. package/src/channel-config.test.ts +0 -39
  38. package/src/channel.adapters.ts +0 -293
  39. package/src/channel.deps.runtime.ts +0 -28
  40. package/src/channel.runtime.ts +0 -17
  41. package/src/channel.setup.ts +0 -98
  42. package/src/channel.test.ts +0 -695
  43. package/src/channel.ts +0 -256
  44. package/src/config-schema.test.ts +0 -16
  45. package/src/config-schema.ts +0 -3
  46. package/src/doctor-contract.test.ts +0 -75
  47. package/src/doctor-contract.ts +0 -182
  48. package/src/doctor.ts +0 -57
  49. package/src/gateway.ts +0 -63
  50. package/src/google-auth.runtime.test.ts +0 -462
  51. package/src/google-auth.runtime.ts +0 -539
  52. package/src/group-policy.ts +0 -17
  53. package/src/monitor-access.test.ts +0 -443
  54. package/src/monitor-access.ts +0 -405
  55. package/src/monitor-reply-delivery.ts +0 -156
  56. package/src/monitor-routing.ts +0 -55
  57. package/src/monitor-types.ts +0 -33
  58. package/src/monitor-webhook.test.ts +0 -446
  59. package/src/monitor-webhook.ts +0 -285
  60. package/src/monitor.reply-delivery.test.ts +0 -139
  61. package/src/monitor.ts +0 -428
  62. package/src/monitor.webhook-routing.test.ts +0 -252
  63. package/src/runtime.ts +0 -9
  64. package/src/secret-contract.test.ts +0 -60
  65. package/src/secret-contract.ts +0 -161
  66. package/src/sender-allow.ts +0 -46
  67. package/src/setup-core.ts +0 -40
  68. package/src/setup-surface.ts +0 -236
  69. package/src/setup.test.ts +0 -560
  70. package/src/targets.test.ts +0 -421
  71. package/src/targets.ts +0 -66
  72. package/src/types.config.ts +0 -3
  73. package/src/types.ts +0 -73
  74. package/test-api.ts +0 -2
  75. package/tsconfig.json +0 -16
package/src/monitor.ts DELETED
@@ -1,428 +0,0 @@
1
- import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
2
- import type { OpenClawConfig } from "../runtime-api.js";
3
- import {
4
- createChannelReplyPipeline,
5
- resolveInboundRouteEnvelopeBuilderWithRuntime,
6
- resolveWebhookPath,
7
- } from "../runtime-api.js";
8
- import { type ResolvedGoogleChatAccount } from "./accounts.js";
9
- import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js";
10
- import { type GoogleChatAudienceType } from "./auth.js";
11
- import { applyGoogleChatInboundAccessPolicy } from "./monitor-access.js";
12
- import { deliverGoogleChatReply } from "./monitor-reply-delivery.js";
13
- import {
14
- registerGoogleChatWebhookTarget,
15
- setGoogleChatWebhookEventProcessor,
16
- } from "./monitor-routing.js";
17
- import type {
18
- GoogleChatCoreRuntime,
19
- GoogleChatMonitorOptions,
20
- GoogleChatRuntimeEnv,
21
- WebhookTarget,
22
- } from "./monitor-types.js";
23
- import { warnAppPrincipalMisconfiguration } from "./monitor-webhook.js";
24
- import { getGoogleChatRuntime } from "./runtime.js";
25
- import type { GoogleChatAttachment, GoogleChatEvent } from "./types.js";
26
-
27
- setGoogleChatWebhookEventProcessor(processGoogleChatEvent);
28
-
29
- function logVerbose(core: GoogleChatCoreRuntime, runtime: GoogleChatRuntimeEnv, message: string) {
30
- if (core.logging.shouldLogVerbose()) {
31
- runtime.log?.(`[googlechat] ${message}`);
32
- }
33
- }
34
-
35
- function normalizeAudienceType(value?: string | null): GoogleChatAudienceType | undefined {
36
- const normalized = normalizeOptionalLowercaseString(value);
37
- if (normalized === "app-url" || normalized === "app_url" || normalized === "app") {
38
- return "app-url";
39
- }
40
- if (
41
- normalized === "project-number" ||
42
- normalized === "project_number" ||
43
- normalized === "project"
44
- ) {
45
- return "project-number";
46
- }
47
- return undefined;
48
- }
49
-
50
- async function processGoogleChatEvent(event: GoogleChatEvent, target: WebhookTarget) {
51
- const eventType = event.type ?? (event as { eventType?: string }).eventType;
52
- if (eventType !== "MESSAGE") {
53
- return;
54
- }
55
- if (!event.message || !event.space) {
56
- return;
57
- }
58
-
59
- await processMessageWithPipeline({
60
- event,
61
- account: target.account,
62
- config: target.config,
63
- runtime: target.runtime,
64
- core: target.core,
65
- statusSink: target.statusSink,
66
- mediaMaxMb: target.mediaMaxMb,
67
- });
68
- }
69
-
70
- /**
71
- * Resolve bot display name with fallback chain:
72
- * 1. Account config name
73
- * 2. Agent name from config
74
- * 3. "OpenClaw" as generic fallback
75
- */
76
- function resolveBotDisplayName(params: {
77
- accountName?: string;
78
- agentId: string;
79
- config: OpenClawConfig;
80
- }): string {
81
- const { accountName, agentId, config } = params;
82
- if (accountName?.trim()) {
83
- return accountName.trim();
84
- }
85
- const agent = config.agents?.list?.find((a) => a.id === agentId);
86
- if (agent?.name?.trim()) {
87
- return agent.name.trim();
88
- }
89
- return "OpenClaw";
90
- }
91
-
92
- async function processMessageWithPipeline(params: {
93
- event: GoogleChatEvent;
94
- account: ResolvedGoogleChatAccount;
95
- config: OpenClawConfig;
96
- runtime: GoogleChatRuntimeEnv;
97
- core: GoogleChatCoreRuntime;
98
- statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
99
- mediaMaxMb: number;
100
- }): Promise<void> {
101
- const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
102
- const space = event.space;
103
- const message = event.message;
104
- if (!space || !message) {
105
- return;
106
- }
107
-
108
- const spaceId = space.name ?? "";
109
- if (!spaceId) {
110
- return;
111
- }
112
- const spaceType = (space.type ?? "").toUpperCase();
113
- const isGroup = spaceType !== "DM";
114
- const sender = message.sender ?? event.user;
115
- const senderId = sender?.name ?? "";
116
- const senderName = sender?.displayName ?? "";
117
- const senderEmail = sender?.email ?? undefined;
118
-
119
- const allowBots = account.config.allowBots === true;
120
- if (!allowBots) {
121
- if (sender?.type?.toUpperCase() === "BOT") {
122
- logVerbose(core, runtime, `skip bot-authored message (${senderId || "unknown"})`);
123
- return;
124
- }
125
- if (senderId === "users/app") {
126
- logVerbose(core, runtime, "skip app-authored message");
127
- return;
128
- }
129
- }
130
-
131
- const messageText = (message.argumentText ?? message.text ?? "").trim();
132
- const attachments = message.attachment ?? [];
133
- const hasMedia = attachments.length > 0;
134
- const rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
135
- if (!rawBody) {
136
- return;
137
- }
138
-
139
- const access = await applyGoogleChatInboundAccessPolicy({
140
- account,
141
- config,
142
- core,
143
- space,
144
- message,
145
- isGroup,
146
- senderId,
147
- senderName,
148
- senderEmail,
149
- rawBody,
150
- statusSink,
151
- logVerbose: (message) => logVerbose(core, runtime, message),
152
- });
153
- if (!access.ok) {
154
- return;
155
- }
156
- const { commandAuthorized, effectiveWasMentioned, groupSystemPrompt } = access;
157
-
158
- const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
159
- cfg: config,
160
- channel: "googlechat",
161
- accountId: account.accountId,
162
- peer: {
163
- kind: isGroup ? ("group" as const) : ("direct" as const),
164
- id: spaceId,
165
- },
166
- runtime: core.channel,
167
- sessionStore: config.session?.store,
168
- });
169
-
170
- let mediaPath: string | undefined;
171
- let mediaType: string | undefined;
172
- if (attachments.length > 0) {
173
- const first = attachments[0];
174
- const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
175
- if (attachmentData) {
176
- mediaPath = attachmentData.path;
177
- mediaType = attachmentData.contentType;
178
- }
179
- }
180
-
181
- const fromLabel = isGroup
182
- ? space.displayName || `space:${spaceId}`
183
- : senderName || `user:${senderId}`;
184
- const { storePath, body } = buildEnvelope({
185
- channel: "Google Chat",
186
- from: fromLabel,
187
- timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
188
- body: rawBody,
189
- });
190
-
191
- const ctxPayload = core.channel.turn.buildContext({
192
- channel: "googlechat",
193
- accountId: route.accountId,
194
- messageId: message.name,
195
- messageIdFull: message.name,
196
- timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
197
- from: `googlechat:${senderId}`,
198
- sender: {
199
- id: senderId,
200
- name: senderName || undefined,
201
- username: senderEmail,
202
- },
203
- conversation: {
204
- kind: isGroup ? "channel" : "direct",
205
- id: spaceId,
206
- label: fromLabel,
207
- routePeer: {
208
- kind: isGroup ? "group" : "direct",
209
- id: spaceId,
210
- },
211
- },
212
- route: {
213
- agentId: route.agentId,
214
- accountId: route.accountId,
215
- routeSessionKey: route.sessionKey,
216
- },
217
- reply: {
218
- to: `googlechat:${spaceId}`,
219
- originatingTo: `googlechat:${spaceId}`,
220
- replyToId: message.thread?.name,
221
- replyToIdFull: message.thread?.name,
222
- },
223
- message: {
224
- body,
225
- bodyForAgent: rawBody,
226
- rawBody,
227
- commandBody: rawBody,
228
- envelopeFrom: fromLabel,
229
- },
230
- media:
231
- mediaPath || mediaType
232
- ? [
233
- {
234
- path: mediaPath,
235
- url: mediaPath,
236
- contentType: mediaType,
237
- },
238
- ]
239
- : undefined,
240
- supplemental: {
241
- groupSystemPrompt: isGroup ? groupSystemPrompt : undefined,
242
- },
243
- extra: {
244
- ChatType: isGroup ? "channel" : "direct",
245
- WasMentioned: isGroup ? effectiveWasMentioned : undefined,
246
- CommandAuthorized: commandAuthorized,
247
- GroupSubject: undefined,
248
- GroupSpace: isGroup ? (space.displayName ?? undefined) : undefined,
249
- },
250
- });
251
-
252
- // Typing indicator setup
253
- // Note: Reaction mode requires user OAuth, not available with service account auth.
254
- // If reaction is configured, we fall back to message mode with a warning.
255
- let typingIndicator = account.config.typingIndicator ?? "message";
256
- if (typingIndicator === "reaction") {
257
- runtime.error?.(
258
- `[${account.accountId}] typingIndicator="reaction" requires user OAuth (not supported with service account). Falling back to "message" mode.`,
259
- );
260
- typingIndicator = "message";
261
- }
262
- let typingMessageName: string | undefined;
263
-
264
- // Start typing indicator (message mode only, reaction mode not supported with app auth)
265
- if (typingIndicator === "message") {
266
- try {
267
- const botName = resolveBotDisplayName({
268
- accountName: account.config.name,
269
- agentId: route.agentId,
270
- config,
271
- });
272
- const result = await sendGoogleChatMessage({
273
- account,
274
- space: spaceId,
275
- text: `_${botName} is typing..._`,
276
- thread: message.thread?.name,
277
- });
278
- typingMessageName = result?.messageName;
279
- } catch (err) {
280
- runtime.error?.(`Failed sending typing message: ${String(err)}`);
281
- }
282
- }
283
-
284
- const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
285
- cfg: config,
286
- agentId: route.agentId,
287
- channel: "googlechat",
288
- accountId: route.accountId,
289
- });
290
-
291
- await core.channel.turn.run({
292
- channel: "googlechat",
293
- accountId: route.accountId,
294
- raw: message,
295
- adapter: {
296
- ingest: () => ({
297
- id: message.name ?? spaceId,
298
- timestamp: event.eventTime ? Date.parse(event.eventTime) : undefined,
299
- rawText: rawBody,
300
- textForAgent: rawBody,
301
- textForCommands: rawBody,
302
- raw: message,
303
- }),
304
- resolveTurn: () => ({
305
- cfg: config,
306
- channel: "googlechat",
307
- accountId: route.accountId,
308
- agentId: route.agentId,
309
- routeSessionKey: route.sessionKey,
310
- storePath,
311
- ctxPayload,
312
- recordInboundSession: core.channel.session.recordInboundSession,
313
- dispatchReplyWithBufferedBlockDispatcher:
314
- core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
315
- delivery: {
316
- deliver: async (payload) => {
317
- await deliverGoogleChatReply({
318
- payload,
319
- account,
320
- spaceId,
321
- runtime,
322
- core,
323
- config,
324
- statusSink,
325
- typingMessageName,
326
- });
327
- // Only use typing message for first delivery
328
- typingMessageName = undefined;
329
- },
330
- onError: (err, info) => {
331
- runtime.error?.(
332
- `[${account.accountId}] Google Chat ${info.kind} reply failed: ${String(err)}`,
333
- );
334
- },
335
- },
336
- dispatcherOptions: replyPipeline,
337
- replyOptions: {
338
- onModelSelected,
339
- },
340
- record: {
341
- onRecordError: (err) => {
342
- runtime.error?.(`googlechat: failed updating session meta: ${String(err)}`);
343
- },
344
- },
345
- }),
346
- },
347
- });
348
- }
349
-
350
- async function downloadAttachment(
351
- attachment: GoogleChatAttachment,
352
- account: ResolvedGoogleChatAccount,
353
- mediaMaxMb: number,
354
- core: GoogleChatCoreRuntime,
355
- ): Promise<{ path: string; contentType?: string } | null> {
356
- const resourceName = attachment.attachmentDataRef?.resourceName;
357
- if (!resourceName) {
358
- return null;
359
- }
360
- const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
361
- const downloaded = await downloadGoogleChatMedia({ account, resourceName, maxBytes });
362
- const saved = await core.channel.media.saveMediaBuffer(
363
- downloaded.buffer,
364
- downloaded.contentType ?? attachment.contentType,
365
- "inbound",
366
- maxBytes,
367
- attachment.contentName,
368
- );
369
- return { path: saved.path, contentType: saved.contentType };
370
- }
371
-
372
- function monitorGoogleChatProvider(options: GoogleChatMonitorOptions): () => void {
373
- const core = getGoogleChatRuntime();
374
- const webhookPath = resolveWebhookPath({
375
- webhookPath: options.webhookPath,
376
- webhookUrl: options.webhookUrl,
377
- defaultPath: "/googlechat",
378
- });
379
- if (!webhookPath) {
380
- options.runtime.error?.(`[${options.account.accountId}] invalid webhook path`);
381
- return () => {};
382
- }
383
-
384
- const audienceType = normalizeAudienceType(options.account.config.audienceType);
385
- const audience = options.account.config.audience?.trim();
386
- const mediaMaxMb = options.account.config.mediaMaxMb ?? 20;
387
-
388
- warnAppPrincipalMisconfiguration({
389
- accountId: options.account.accountId,
390
- audienceType,
391
- appPrincipal: options.account.config.appPrincipal,
392
- log: options.runtime.log,
393
- });
394
-
395
- const unregisterTarget = registerGoogleChatWebhookTarget({
396
- account: options.account,
397
- config: options.config,
398
- runtime: options.runtime,
399
- core,
400
- path: webhookPath,
401
- audienceType,
402
- audience,
403
- statusSink: options.statusSink,
404
- mediaMaxMb,
405
- });
406
-
407
- return () => {
408
- unregisterTarget();
409
- };
410
- }
411
-
412
- export async function startGoogleChatMonitor(
413
- params: GoogleChatMonitorOptions,
414
- ): Promise<() => void> {
415
- return monitorGoogleChatProvider(params);
416
- }
417
-
418
- export function resolveGoogleChatWebhookPath(params: {
419
- account: ResolvedGoogleChatAccount;
420
- }): string {
421
- return (
422
- resolveWebhookPath({
423
- webhookPath: params.account.config.webhookPath,
424
- webhookUrl: params.account.config.webhookUrl,
425
- defaultPath: "/googlechat",
426
- }) ?? "/googlechat"
427
- );
428
- }
@@ -1,252 +0,0 @@
1
- import { EventEmitter } from "node:events";
2
- import type { IncomingMessage } from "node:http";
3
- import {
4
- createEmptyPluginRegistry,
5
- setActivePluginRegistry,
6
- } from "openclaw/plugin-sdk/plugin-test-runtime";
7
- import { createMockServerResponse } from "openclaw/plugin-sdk/test-env";
8
- import { afterEach, describe, expect, it, vi } from "vitest";
9
- import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
10
- import type { ResolvedGoogleChatAccount } from "./accounts.js";
11
- import { verifyGoogleChatRequest } from "./auth.js";
12
- import {
13
- handleGoogleChatWebhookRequest,
14
- registerGoogleChatWebhookTarget,
15
- } from "./monitor-routing.js";
16
-
17
- vi.mock("./auth.js", () => ({
18
- verifyGoogleChatRequest: vi.fn(),
19
- }));
20
-
21
- function createWebhookRequest(params: {
22
- authorization?: string;
23
- payload: unknown;
24
- path?: string;
25
- }): IncomingMessage {
26
- const req = new EventEmitter() as IncomingMessage & {
27
- destroyed?: boolean;
28
- destroy: (error?: Error) => IncomingMessage;
29
- on: (event: string, listener: (...args: unknown[]) => void) => IncomingMessage;
30
- };
31
- req.method = "POST";
32
- req.url = params.path ?? "/googlechat";
33
- req.headers = {
34
- authorization: params.authorization ?? "",
35
- "content-type": "application/json",
36
- };
37
- req.destroyed = false;
38
- (req as unknown as { socket: { remoteAddress: string } }).socket = {
39
- remoteAddress: "127.0.0.1",
40
- };
41
- req.destroy = () => {
42
- req.destroyed = true;
43
- return req;
44
- };
45
-
46
- const originalOn = req.on.bind(req);
47
- let bodyScheduled = false;
48
- req.on = ((event: string, listener: (...args: unknown[]) => void) => {
49
- const result = originalOn(event, listener);
50
- if (!bodyScheduled && event === "data") {
51
- bodyScheduled = true;
52
- void Promise.resolve().then(() => {
53
- req.emit("data", Buffer.from(JSON.stringify(params.payload), "utf-8"));
54
- if (!req.destroyed) {
55
- req.emit("end");
56
- }
57
- });
58
- }
59
- return result;
60
- }) as IncomingMessage["on"];
61
-
62
- return req;
63
- }
64
-
65
- function createHeaderOnlyWebhookRequest(params: {
66
- authorization?: string;
67
- path?: string;
68
- }): IncomingMessage {
69
- const req = new EventEmitter() as IncomingMessage;
70
- req.method = "POST";
71
- req.url = params.path ?? "/googlechat";
72
- req.headers = {
73
- authorization: params.authorization ?? "",
74
- "content-type": "application/json",
75
- };
76
- (req as unknown as { socket: { remoteAddress: string } }).socket = {
77
- remoteAddress: "127.0.0.1",
78
- };
79
- return req;
80
- }
81
-
82
- const baseAccount = (accountId: string) =>
83
- ({
84
- accountId,
85
- enabled: true,
86
- credentialSource: "none",
87
- config: {},
88
- }) as ResolvedGoogleChatAccount;
89
-
90
- function registerTwoTargets() {
91
- const sinkA = vi.fn();
92
- const sinkB = vi.fn();
93
- const logA = vi.fn();
94
- const logB = vi.fn();
95
- const core = {} as PluginRuntime;
96
- const config = {} as OpenClawConfig;
97
-
98
- const unregisterA = registerGoogleChatWebhookTarget({
99
- account: baseAccount("A"),
100
- config,
101
- runtime: { log: logA },
102
- core,
103
- path: "/googlechat",
104
- statusSink: sinkA,
105
- mediaMaxMb: 5,
106
- });
107
- const unregisterB = registerGoogleChatWebhookTarget({
108
- account: baseAccount("B"),
109
- config,
110
- runtime: { log: logB },
111
- core,
112
- path: "/googlechat",
113
- statusSink: sinkB,
114
- mediaMaxMb: 5,
115
- });
116
-
117
- return {
118
- logA,
119
- logB,
120
- sinkA,
121
- sinkB,
122
- unregister: () => {
123
- unregisterA();
124
- unregisterB();
125
- },
126
- };
127
- }
128
-
129
- async function dispatchWebhookRequest(req: IncomingMessage) {
130
- const res = createMockServerResponse();
131
- const handled = await handleGoogleChatWebhookRequest(req, res);
132
- expect(handled).toBe(true);
133
- return res;
134
- }
135
-
136
- async function expectVerifiedRoute(params: {
137
- request: IncomingMessage;
138
- expectedStatus: number;
139
- sinkA: ReturnType<typeof vi.fn>;
140
- sinkB: ReturnType<typeof vi.fn>;
141
- expectedSink: "none" | "A" | "B";
142
- }) {
143
- const res = await dispatchWebhookRequest(params.request);
144
- expect(res.statusCode).toBe(params.expectedStatus);
145
- const expectedCounts =
146
- params.expectedSink === "A" ? [1, 0] : params.expectedSink === "B" ? [0, 1] : [0, 0];
147
- expect(params.sinkA).toHaveBeenCalledTimes(expectedCounts[0]);
148
- expect(params.sinkB).toHaveBeenCalledTimes(expectedCounts[1]);
149
- }
150
-
151
- function mockSecondVerifierSuccess() {
152
- vi.mocked(verifyGoogleChatRequest)
153
- .mockResolvedValueOnce({ ok: false, reason: "invalid" })
154
- .mockResolvedValueOnce({ ok: true });
155
- }
156
-
157
- describe("Google Chat webhook routing", () => {
158
- afterEach(() => {
159
- setActivePluginRegistry(createEmptyPluginRegistry());
160
- });
161
-
162
- it("rejects ambiguous routing when multiple targets on the same path verify successfully", async () => {
163
- vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: true });
164
-
165
- const { sinkA, sinkB, unregister } = registerTwoTargets();
166
-
167
- try {
168
- await expectVerifiedRoute({
169
- request: createWebhookRequest({
170
- authorization: "Bearer test-token",
171
- payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/AAA" } },
172
- }),
173
- expectedStatus: 401,
174
- sinkA,
175
- sinkB,
176
- expectedSink: "none",
177
- });
178
- } finally {
179
- unregister();
180
- }
181
- });
182
-
183
- it("routes to the single verified target when earlier targets fail verification", async () => {
184
- mockSecondVerifierSuccess();
185
-
186
- const { logA, logB, sinkA, sinkB, unregister } = registerTwoTargets();
187
-
188
- try {
189
- await expectVerifiedRoute({
190
- request: createWebhookRequest({
191
- authorization: "Bearer test-token",
192
- payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/BBB" } },
193
- }),
194
- expectedStatus: 200,
195
- sinkA,
196
- sinkB,
197
- expectedSink: "B",
198
- });
199
- expect(logA).not.toHaveBeenCalled();
200
- expect(logB).not.toHaveBeenCalled();
201
- } finally {
202
- unregister();
203
- }
204
- });
205
-
206
- it("rejects invalid bearer before attempting to read the body", async () => {
207
- vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: false, reason: "invalid" });
208
- const { unregister } = registerTwoTargets();
209
-
210
- try {
211
- const req = createHeaderOnlyWebhookRequest({
212
- authorization: "Bearer invalid-token",
213
- });
214
- const onSpy = vi.spyOn(req, "on");
215
- const res = await dispatchWebhookRequest(req);
216
- expect(res.statusCode).toBe(401);
217
- expect(onSpy).not.toHaveBeenCalledWith("data", expect.any(Function));
218
- } finally {
219
- unregister();
220
- }
221
- });
222
-
223
- it("supports add-on requests that provide systemIdToken in the body", async () => {
224
- mockSecondVerifierSuccess();
225
- const { sinkA, sinkB, unregister } = registerTwoTargets();
226
-
227
- try {
228
- await expectVerifiedRoute({
229
- request: createWebhookRequest({
230
- payload: {
231
- commonEventObject: { hostApp: "CHAT" },
232
- authorizationEventObject: { systemIdToken: "addon-token" },
233
- chat: {
234
- eventTime: "2026-03-02T00:00:00.000Z",
235
- user: { name: "users/12345", displayName: "Test User" },
236
- messagePayload: {
237
- space: { name: "spaces/AAA" },
238
- message: { text: "Hello from add-on" },
239
- },
240
- },
241
- },
242
- }),
243
- expectedStatus: 200,
244
- sinkA,
245
- sinkB,
246
- expectedSink: "B",
247
- });
248
- } finally {
249
- unregister();
250
- }
251
- });
252
- });
package/src/runtime.ts DELETED
@@ -1,9 +0,0 @@
1
- import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
- import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
3
-
4
- const { setRuntime: setGoogleChatRuntime, getRuntime: getGoogleChatRuntime } =
5
- createPluginRuntimeStore<PluginRuntime>({
6
- pluginId: "googlechat",
7
- errorMessage: "Google Chat runtime not initialized",
8
- });
9
- export { getGoogleChatRuntime, setGoogleChatRuntime };