@openclaw/googlechat 2026.5.2-beta.2 → 2026.5.3-beta.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.
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
@@ -0,0 +1,920 @@
1
+ import { A as resolveInboundRouteEnvelopeBuilderWithRuntime, D as resolveDefaultGroupPolicy, F as warnMissingProviderGroupPolicyFallbackOnce, L as getGoogleChatRuntime, M as resolveWebhookPath, O as resolveDmGroupAccessWithLists, T as resolveAllowlistProviderRuntimeGroupPolicy, f as evaluateGroupRouteAccessForPolicy, g as isDangerousNameMatchingEnabled, j as resolveSenderScopedGroupPolicy, l as createChannelPairingController, n as GROUP_POLICY_BLOCKED_LABEL, u as createChannelReplyPipeline } from "./runtime-api-wkIdfwqY.js";
2
+ import { c as sendGoogleChatMessage, d as verifyGoogleChatRequest, i as downloadGoogleChatMedia, l as updateGoogleChatMessage, n as deleteGoogleChatMessage, s as probeGoogleChat, u as uploadGoogleChatAttachment } from "./api-C67jv0ow.js";
3
+ import { normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
4
+ import { resolveInboundMentionDecision } from "openclaw/plugin-sdk/channel-inbound";
5
+ import { registerWebhookTargetWithPluginRoute, resolveWebhookTargetWithAuthOrReject, withResolvedWebhookRequestPipeline } from "openclaw/plugin-sdk/webhook-targets";
6
+ import { createWebhookInFlightLimiter, readJsonWebhookBodyOrReject } from "openclaw/plugin-sdk/webhook-request-guards";
7
+ import { expandAllowFromWithAccessGroups } from "openclaw/plugin-sdk/security-runtime";
8
+ import { deliverTextOrMediaReply, resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
9
+ //#region extensions/googlechat/src/sender-allow.ts
10
+ function normalizeUserId$1(raw) {
11
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
12
+ if (!trimmed) return "";
13
+ return normalizeLowercaseStringOrEmpty(trimmed.replace(/^users\//i, ""));
14
+ }
15
+ function isEmailLike(value) {
16
+ return value.includes("@");
17
+ }
18
+ function isSenderAllowed(senderId, senderEmail, allowFrom, allowNameMatching = false) {
19
+ if (allowFrom.includes("*")) return true;
20
+ const normalizedSenderId = normalizeUserId$1(senderId);
21
+ const normalizedEmail = normalizeLowercaseStringOrEmpty(senderEmail ?? "");
22
+ return allowFrom.some((entry) => {
23
+ const normalized = normalizeLowercaseStringOrEmpty(entry);
24
+ if (!normalized) return false;
25
+ const withoutPrefix = normalized.replace(/^(googlechat|google-chat|gchat):/i, "");
26
+ if (withoutPrefix.startsWith("users/")) return normalizeUserId$1(withoutPrefix) === normalizedSenderId;
27
+ if (allowNameMatching && normalizedEmail && isEmailLike(withoutPrefix)) return withoutPrefix === normalizedEmail;
28
+ return withoutPrefix.replace(/^users\//i, "") === normalizedSenderId;
29
+ });
30
+ }
31
+ //#endregion
32
+ //#region extensions/googlechat/src/monitor-access.ts
33
+ function normalizeUserId(raw) {
34
+ const trimmed = normalizeOptionalString(raw) ?? "";
35
+ if (!trimmed) return "";
36
+ return normalizeLowercaseStringOrEmpty(trimmed.replace(/^users\//i, ""));
37
+ }
38
+ function resolveGroupConfig(params) {
39
+ const { groupId, groupName, groups } = params;
40
+ const entries = groups ?? {};
41
+ const keys = Object.keys(entries);
42
+ if (keys.length === 0) return {
43
+ entry: void 0,
44
+ allowlistConfigured: false,
45
+ deprecatedNameMatch: false
46
+ };
47
+ const entry = entries[groupId];
48
+ const normalizedGroupName = normalizeLowercaseStringOrEmpty(groupName ?? "");
49
+ const deprecatedNameMatch = !entry && Boolean(groupName && keys.some((key) => {
50
+ const trimmed = key.trim();
51
+ if (!trimmed || trimmed === "*" || /^spaces\//i.test(trimmed)) return false;
52
+ return trimmed === groupName || normalizeLowercaseStringOrEmpty(trimmed) === normalizedGroupName;
53
+ }));
54
+ const fallback = entries["*"];
55
+ return {
56
+ entry: deprecatedNameMatch ? void 0 : entry ?? fallback,
57
+ allowlistConfigured: true,
58
+ fallback,
59
+ deprecatedNameMatch
60
+ };
61
+ }
62
+ function extractMentionInfo(annotations, botUser) {
63
+ const mentionAnnotations = annotations.filter((entry) => entry.type === "USER_MENTION");
64
+ const hasAnyMention = mentionAnnotations.length > 0;
65
+ const botTargets = new Set(["users/app", botUser?.trim()].filter(Boolean));
66
+ return {
67
+ hasAnyMention,
68
+ wasMentioned: mentionAnnotations.some((entry) => {
69
+ const userName = entry.userMention?.user?.name;
70
+ if (!userName) return false;
71
+ if (botTargets.has(userName)) return true;
72
+ return normalizeUserId(userName) === "app";
73
+ })
74
+ };
75
+ }
76
+ const warnedDeprecatedUsersEmailAllowFrom = /* @__PURE__ */ new Set();
77
+ const warnedMutableGroupKeys = /* @__PURE__ */ new Set();
78
+ function warnDeprecatedUsersEmailEntries(logVerbose, entries) {
79
+ const deprecated = entries.map((v) => normalizeOptionalString(v)).filter((v) => Boolean(v)).filter((v) => /^users\/.+@.+/i.test(v));
80
+ if (deprecated.length === 0) return;
81
+ const key = deprecated.map((v) => normalizeLowercaseStringOrEmpty(v)).toSorted((a, b) => a.localeCompare(b)).join(",");
82
+ if (warnedDeprecatedUsersEmailAllowFrom.has(key)) return;
83
+ warnedDeprecatedUsersEmailAllowFrom.add(key);
84
+ logVerbose(`Deprecated allowFrom entry detected: "users/<email>" is no longer treated as an email allowlist. Use raw email (alice@example.com) or immutable user id (users/<id>). entries=${deprecated.join(", ")}`);
85
+ }
86
+ function warnMutableGroupKeysConfigured(logVerbose, groups) {
87
+ const mutableKeys = Object.keys(groups ?? {}).map((key) => key.trim()).filter((key) => key && key !== "*" && !/^spaces\//i.test(key));
88
+ if (mutableKeys.length === 0) return;
89
+ const warningKey = mutableKeys.map((key) => normalizeLowercaseStringOrEmpty(key)).toSorted((a, b) => a.localeCompare(b)).join(",");
90
+ if (warnedMutableGroupKeys.has(warningKey)) return;
91
+ warnedMutableGroupKeys.add(warningKey);
92
+ logVerbose(`Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: ${mutableKeys.join(", ")}`);
93
+ }
94
+ async function applyGoogleChatInboundAccessPolicy(params) {
95
+ const { account, config, core, space, message, isGroup, senderId, senderName, senderEmail, rawBody, statusSink, logVerbose } = params;
96
+ const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
97
+ const spaceId = space.name ?? "";
98
+ const pairing = createChannelPairingController({
99
+ core,
100
+ channel: "googlechat",
101
+ accountId: account.accountId
102
+ });
103
+ const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
104
+ const { groupPolicy, providerMissingFallbackApplied } = resolveAllowlistProviderRuntimeGroupPolicy({
105
+ providerConfigPresent: config.channels?.googlechat !== void 0,
106
+ groupPolicy: account.config.groupPolicy,
107
+ defaultGroupPolicy
108
+ });
109
+ warnMissingProviderGroupPolicyFallbackOnce({
110
+ providerMissingFallbackApplied,
111
+ providerKey: "googlechat",
112
+ accountId: account.accountId,
113
+ blockedLabel: GROUP_POLICY_BLOCKED_LABEL.space,
114
+ log: logVerbose
115
+ });
116
+ warnMutableGroupKeysConfigured(logVerbose, account.config.groups ?? void 0);
117
+ const groupConfigResolved = resolveGroupConfig({
118
+ groupId: spaceId,
119
+ groupName: space.displayName ?? null,
120
+ groups: account.config.groups ?? void 0
121
+ });
122
+ const groupEntry = groupConfigResolved.entry;
123
+ const groupUsers = groupEntry?.users ?? account.config.groupAllowFrom ?? [];
124
+ const isGoogleChatSenderAllowed = (_senderId, allowFrom) => isSenderAllowed(senderId, senderEmail, allowFrom, allowNameMatching);
125
+ const expandedGroupUsers = await expandAllowFromWithAccessGroups({
126
+ cfg: config,
127
+ allowFrom: groupUsers,
128
+ channel: "googlechat",
129
+ accountId: account.accountId,
130
+ senderId,
131
+ isSenderAllowed: isGoogleChatSenderAllowed
132
+ });
133
+ let effectiveWasMentioned;
134
+ if (isGroup) {
135
+ if (groupConfigResolved.deprecatedNameMatch) {
136
+ logVerbose(`drop group message (deprecated mutable group key matched, space=${spaceId})`);
137
+ return { ok: false };
138
+ }
139
+ const groupAllowlistConfigured = groupConfigResolved.allowlistConfigured;
140
+ const routeAccess = evaluateGroupRouteAccessForPolicy({
141
+ groupPolicy,
142
+ routeAllowlistConfigured: groupAllowlistConfigured,
143
+ routeMatched: Boolean(groupEntry),
144
+ routeEnabled: groupEntry?.enabled !== false
145
+ });
146
+ if (!routeAccess.allowed) {
147
+ if (routeAccess.reason === "disabled") logVerbose(`drop group message (groupPolicy=disabled, space=${spaceId})`);
148
+ else if (routeAccess.reason === "empty_allowlist") logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
149
+ else if (routeAccess.reason === "route_not_allowlisted") logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
150
+ else if (routeAccess.reason === "route_disabled") logVerbose(`drop group message (space disabled, space=${spaceId})`);
151
+ return { ok: false };
152
+ }
153
+ if (expandedGroupUsers.length > 0) {
154
+ warnDeprecatedUsersEmailEntries(logVerbose, expandedGroupUsers);
155
+ if (!isSenderAllowed(senderId, senderEmail, expandedGroupUsers, allowNameMatching)) {
156
+ logVerbose(`drop group message (sender not allowed, ${senderId})`);
157
+ return { ok: false };
158
+ }
159
+ }
160
+ }
161
+ const dmPolicy = account.config.dm?.policy ?? "pairing";
162
+ const rawConfigAllowFrom = (account.config.dm?.allowFrom ?? []).map((v) => String(v));
163
+ const normalizedGroupUsers = expandedGroupUsers;
164
+ const senderGroupPolicy = groupConfigResolved.allowlistConfigured && normalizedGroupUsers.length === 0 ? groupPolicy : resolveSenderScopedGroupPolicy({
165
+ groupPolicy,
166
+ groupAllowFrom: normalizedGroupUsers
167
+ });
168
+ const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
169
+ const storeAllowFrom = !isGroup && dmPolicy !== "allowlist" && dmPolicy !== "open" ? await pairing.readAllowFromStore().catch(() => []) : [];
170
+ const [configAllowFrom, effectiveStoreAllowFrom] = await Promise.all([expandAllowFromWithAccessGroups({
171
+ cfg: config,
172
+ allowFrom: rawConfigAllowFrom,
173
+ channel: "googlechat",
174
+ accountId: account.accountId,
175
+ senderId,
176
+ isSenderAllowed: isGoogleChatSenderAllowed
177
+ }), expandAllowFromWithAccessGroups({
178
+ cfg: config,
179
+ allowFrom: storeAllowFrom,
180
+ channel: "googlechat",
181
+ accountId: account.accountId,
182
+ senderId,
183
+ isSenderAllowed: isGoogleChatSenderAllowed
184
+ })]);
185
+ const access = resolveDmGroupAccessWithLists({
186
+ isGroup,
187
+ dmPolicy,
188
+ groupPolicy: senderGroupPolicy,
189
+ allowFrom: configAllowFrom,
190
+ groupAllowFrom: normalizedGroupUsers,
191
+ storeAllowFrom: effectiveStoreAllowFrom,
192
+ groupAllowFromFallbackToAllowFrom: false,
193
+ isSenderAllowed: (allowFrom) => isSenderAllowed(senderId, senderEmail, allowFrom, allowNameMatching)
194
+ });
195
+ const effectiveAllowFrom = access.effectiveAllowFrom;
196
+ const effectiveGroupAllowFrom = access.effectiveGroupAllowFrom;
197
+ warnDeprecatedUsersEmailEntries(logVerbose, effectiveAllowFrom);
198
+ const commandAllowFrom = isGroup ? effectiveGroupAllowFrom : effectiveAllowFrom;
199
+ const useAccessGroups = config.commands?.useAccessGroups !== false;
200
+ const senderAllowedForCommands = isSenderAllowed(senderId, senderEmail, commandAllowFrom, allowNameMatching);
201
+ const commandAuthorized = shouldComputeAuth ? core.channel.commands.resolveCommandAuthorizedFromAuthorizers({
202
+ useAccessGroups,
203
+ authorizers: [{
204
+ configured: commandAllowFrom.length > 0,
205
+ allowed: senderAllowedForCommands
206
+ }]
207
+ }) : void 0;
208
+ if (isGroup) {
209
+ const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
210
+ const mentionInfo = extractMentionInfo(message.annotations ?? [], account.config.botUser);
211
+ const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
212
+ cfg: config,
213
+ surface: "googlechat"
214
+ });
215
+ const mentionDecision = resolveInboundMentionDecision({
216
+ facts: {
217
+ canDetectMention: true,
218
+ wasMentioned: mentionInfo.wasMentioned,
219
+ hasAnyMention: mentionInfo.hasAnyMention,
220
+ implicitMentionKinds: []
221
+ },
222
+ policy: {
223
+ isGroup: true,
224
+ requireMention,
225
+ allowTextCommands,
226
+ hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
227
+ commandAuthorized: commandAuthorized === true
228
+ }
229
+ });
230
+ effectiveWasMentioned = mentionDecision.effectiveWasMentioned;
231
+ if (mentionDecision.shouldSkip) {
232
+ logVerbose(`drop group message (mention required, space=${spaceId})`);
233
+ return { ok: false };
234
+ }
235
+ }
236
+ if (isGroup && access.decision !== "allow") {
237
+ logVerbose(`drop group message (sender policy blocked, reason=${access.reason}, space=${spaceId})`);
238
+ return { ok: false };
239
+ }
240
+ if (!isGroup) {
241
+ if (account.config.dm?.enabled === false) {
242
+ logVerbose(`Blocked Google Chat DM from ${senderId} (dmPolicy=disabled)`);
243
+ return { ok: false };
244
+ }
245
+ if (access.decision !== "allow") {
246
+ if (access.decision === "pairing") await pairing.issueChallenge({
247
+ senderId,
248
+ senderIdLine: `Your Google Chat user id: ${senderId}`,
249
+ meta: {
250
+ name: senderName || void 0,
251
+ email: senderEmail
252
+ },
253
+ onCreated: () => {
254
+ logVerbose(`googlechat pairing request sender=${senderId}`);
255
+ },
256
+ sendPairingReply: async (text) => {
257
+ await sendGoogleChatMessage({
258
+ account,
259
+ space: spaceId,
260
+ text
261
+ });
262
+ statusSink?.({ lastOutboundAt: Date.now() });
263
+ },
264
+ onReplyError: (err) => {
265
+ logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
266
+ }
267
+ });
268
+ else logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
269
+ return { ok: false };
270
+ }
271
+ }
272
+ if (isGroup && core.channel.commands.isControlCommandMessage(rawBody, config) && commandAuthorized !== true) {
273
+ logVerbose(`googlechat: drop control command from ${senderId}`);
274
+ return { ok: false };
275
+ }
276
+ return {
277
+ ok: true,
278
+ commandAuthorized,
279
+ effectiveWasMentioned,
280
+ groupSystemPrompt: normalizeOptionalString(groupEntry?.systemPrompt)
281
+ };
282
+ }
283
+ //#endregion
284
+ //#region extensions/googlechat/src/monitor-reply-delivery.ts
285
+ async function deliverGoogleChatReply(params) {
286
+ const { payload, account, spaceId, runtime, core, config, statusSink } = params;
287
+ let typingMessageName = params.typingMessageName;
288
+ const reply = resolveSendableOutboundReplyParts(payload);
289
+ const mediaCount = reply.mediaCount;
290
+ const hasMedia = reply.hasMedia;
291
+ const text = reply.text;
292
+ let firstTextChunk = true;
293
+ let suppressCaption = false;
294
+ if (hasMedia && typingMessageName) try {
295
+ await deleteGoogleChatMessage({
296
+ account,
297
+ messageName: typingMessageName
298
+ });
299
+ typingMessageName = void 0;
300
+ } catch (err) {
301
+ runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
302
+ if (typingMessageName) {
303
+ const fallbackText = reply.hasText ? text : mediaCount > 1 ? "Sent attachments." : "Sent attachment.";
304
+ try {
305
+ await updateGoogleChatMessage({
306
+ account,
307
+ messageName: typingMessageName,
308
+ text: fallbackText
309
+ });
310
+ suppressCaption = Boolean(text.trim());
311
+ } catch (updateErr) {
312
+ runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
313
+ typingMessageName = void 0;
314
+ }
315
+ }
316
+ }
317
+ const chunkLimit = account.config.textChunkLimit ?? 4e3;
318
+ const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
319
+ const sendTextMessage = async (chunk) => {
320
+ await sendGoogleChatMessage({
321
+ account,
322
+ space: spaceId,
323
+ text: chunk,
324
+ thread: payload.replyToId
325
+ });
326
+ };
327
+ await deliverTextOrMediaReply({
328
+ payload,
329
+ text: suppressCaption ? "" : reply.text,
330
+ chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, chunkLimit, chunkMode),
331
+ sendText: async (chunk) => {
332
+ try {
333
+ if (firstTextChunk && typingMessageName) await updateGoogleChatMessage({
334
+ account,
335
+ messageName: typingMessageName,
336
+ text: chunk
337
+ });
338
+ else await sendTextMessage(chunk);
339
+ firstTextChunk = false;
340
+ statusSink?.({ lastOutboundAt: Date.now() });
341
+ } catch (err) {
342
+ runtime.error?.(`Google Chat message send failed: ${String(err)}`);
343
+ if (firstTextChunk && typingMessageName) {
344
+ typingMessageName = void 0;
345
+ try {
346
+ await sendTextMessage(chunk);
347
+ statusSink?.({ lastOutboundAt: Date.now() });
348
+ } catch (fallbackErr) {
349
+ runtime.error?.(`Google Chat message fallback send failed: ${String(fallbackErr)}`);
350
+ } finally {
351
+ firstTextChunk = false;
352
+ }
353
+ }
354
+ }
355
+ },
356
+ sendMedia: async ({ mediaUrl, caption }) => {
357
+ try {
358
+ const loaded = await core.channel.media.fetchRemoteMedia({
359
+ url: mediaUrl,
360
+ maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024
361
+ });
362
+ const upload = await uploadAttachmentForReply({
363
+ account,
364
+ spaceId,
365
+ buffer: loaded.buffer,
366
+ contentType: loaded.contentType,
367
+ filename: loaded.fileName ?? "attachment"
368
+ });
369
+ if (!upload.attachmentUploadToken) throw new Error("missing attachment upload token");
370
+ await sendGoogleChatMessage({
371
+ account,
372
+ space: spaceId,
373
+ text: caption,
374
+ thread: payload.replyToId,
375
+ attachments: [{
376
+ attachmentUploadToken: upload.attachmentUploadToken,
377
+ contentName: loaded.fileName
378
+ }]
379
+ });
380
+ statusSink?.({ lastOutboundAt: Date.now() });
381
+ } catch (err) {
382
+ runtime.error?.(`Google Chat attachment send failed: ${String(err)}`);
383
+ }
384
+ }
385
+ });
386
+ }
387
+ async function uploadAttachmentForReply(params) {
388
+ const { account, spaceId, buffer, contentType, filename } = params;
389
+ return await uploadGoogleChatAttachment({
390
+ account,
391
+ space: spaceId,
392
+ filename,
393
+ buffer,
394
+ contentType
395
+ });
396
+ }
397
+ //#endregion
398
+ //#region extensions/googlechat/src/monitor-webhook.ts
399
+ function extractBearerToken(header) {
400
+ const authHeader = Array.isArray(header) ? typeof header[0] === "string" ? header[0] : "" : typeof header === "string" ? header : "";
401
+ return normalizeLowercaseStringOrEmpty(authHeader).startsWith("bearer ") ? authHeader.slice(7).trim() : "";
402
+ }
403
+ const ADD_ON_PREAUTH_MAX_BYTES = 16 * 1024;
404
+ const ADD_ON_PREAUTH_TIMEOUT_MS = 3e3;
405
+ function parseGoogleChatInboundPayload(raw, res) {
406
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
407
+ res.statusCode = 400;
408
+ res.end("invalid payload");
409
+ return { ok: false };
410
+ }
411
+ let eventPayload = raw;
412
+ let addOnBearerToken = "";
413
+ const rawObj = raw;
414
+ if (rawObj.commonEventObject?.hostApp === "CHAT" && rawObj.chat?.messagePayload) {
415
+ const chat = rawObj.chat;
416
+ const messagePayload = chat.messagePayload;
417
+ eventPayload = {
418
+ type: "MESSAGE",
419
+ space: messagePayload?.space,
420
+ message: messagePayload?.message,
421
+ user: chat.user,
422
+ eventTime: chat.eventTime
423
+ };
424
+ addOnBearerToken = typeof rawObj.authorizationEventObject?.systemIdToken === "string" ? rawObj.authorizationEventObject.systemIdToken.trim() : "";
425
+ }
426
+ const event = eventPayload;
427
+ const eventType = event.type ?? eventPayload.eventType;
428
+ if (typeof eventType !== "string") {
429
+ res.statusCode = 400;
430
+ res.end("invalid payload");
431
+ return { ok: false };
432
+ }
433
+ if (!event.space || typeof event.space !== "object" || Array.isArray(event.space)) {
434
+ res.statusCode = 400;
435
+ res.end("invalid payload");
436
+ return { ok: false };
437
+ }
438
+ if (eventType === "MESSAGE") {
439
+ if (!event.message || typeof event.message !== "object" || Array.isArray(event.message)) {
440
+ res.statusCode = 400;
441
+ res.end("invalid payload");
442
+ return { ok: false };
443
+ }
444
+ }
445
+ return {
446
+ ok: true,
447
+ event,
448
+ addOnBearerToken
449
+ };
450
+ }
451
+ async function verifyGoogleChatTargetAuth(target, bearer) {
452
+ const verification = await verifyGoogleChatRequest({
453
+ bearer,
454
+ audienceType: target.audienceType,
455
+ audience: target.audience,
456
+ expectedAddOnPrincipal: target.account.config.appPrincipal
457
+ });
458
+ return verification.ok ? { ok: true } : {
459
+ ok: false,
460
+ reason: verification.reason ?? "unknown"
461
+ };
462
+ }
463
+ function logGoogleChatWebhookAuthRejections(rejections) {
464
+ for (const rejection of rejections) rejection.target.runtime.log?.(`[${rejection.target.account.accountId}] Google Chat webhook auth rejected: ${rejection.reason}`);
465
+ }
466
+ function logGoogleChatWebhookAuthRejectedForTargets(targets, reason) {
467
+ logGoogleChatWebhookAuthRejections(targets.map((target) => ({
468
+ target,
469
+ reason
470
+ })));
471
+ }
472
+ async function resolveGoogleChatWebhookTargetWithAuthOrReject(params) {
473
+ const rejections = [];
474
+ let verifiedTargetCount = 0;
475
+ const selectedTarget = await resolveWebhookTargetWithAuthOrReject({
476
+ targets: params.targets,
477
+ res: params.res,
478
+ isMatch: async (target) => {
479
+ const verification = await verifyGoogleChatTargetAuth(target, params.bearer);
480
+ if (verification.ok) {
481
+ verifiedTargetCount += 1;
482
+ return true;
483
+ }
484
+ rejections.push({
485
+ target,
486
+ reason: verification.reason
487
+ });
488
+ return false;
489
+ }
490
+ });
491
+ if (!selectedTarget && verifiedTargetCount === 0) logGoogleChatWebhookAuthRejections(rejections);
492
+ return selectedTarget;
493
+ }
494
+ function warnAppPrincipalMisconfiguration(params) {
495
+ if (params.audienceType !== "app-url") return;
496
+ const principal = params.appPrincipal?.trim();
497
+ if (!principal) params.log?.(`[${params.accountId}] appPrincipal is missing for audienceType "app-url"; add-on token verification will fail. Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email.`);
498
+ else if (principal.includes("@")) params.log?.(`[${params.accountId}] appPrincipal "${principal}" looks like an email address. Set appPrincipal to the numeric OAuth 2.0 client ID (uniqueId, 21 digits), not an email.`);
499
+ }
500
+ function createGoogleChatWebhookRequestHandler(params) {
501
+ return async (req, res) => {
502
+ return await withResolvedWebhookRequestPipeline({
503
+ req,
504
+ res,
505
+ targetsByPath: params.webhookTargets,
506
+ allowMethods: ["POST"],
507
+ requireJsonContentType: true,
508
+ inFlightLimiter: params.webhookInFlightLimiter,
509
+ handle: async ({ targets }) => {
510
+ const headerBearer = extractBearerToken(req.headers.authorization);
511
+ let selectedTarget = null;
512
+ let parsedEvent = null;
513
+ const readAndParseEvent = async (profile) => {
514
+ const body = await readJsonWebhookBodyOrReject({
515
+ req,
516
+ res,
517
+ profile,
518
+ ...profile === "pre-auth" ? {
519
+ maxBytes: ADD_ON_PREAUTH_MAX_BYTES,
520
+ timeoutMs: ADD_ON_PREAUTH_TIMEOUT_MS
521
+ } : {},
522
+ emptyObjectOnEmpty: false,
523
+ invalidJsonMessage: "invalid payload"
524
+ });
525
+ if (!body.ok) return null;
526
+ const parsed = parseGoogleChatInboundPayload(body.value, res);
527
+ return parsed.ok ? parsed : null;
528
+ };
529
+ if (headerBearer) {
530
+ selectedTarget = await resolveGoogleChatWebhookTargetWithAuthOrReject({
531
+ targets,
532
+ res,
533
+ bearer: headerBearer
534
+ });
535
+ if (!selectedTarget) return true;
536
+ const parsed = await readAndParseEvent("post-auth");
537
+ if (!parsed) return true;
538
+ parsedEvent = parsed.event;
539
+ } else {
540
+ const parsed = await readAndParseEvent("pre-auth");
541
+ if (!parsed) return true;
542
+ parsedEvent = parsed.event;
543
+ if (!parsed.addOnBearerToken) {
544
+ logGoogleChatWebhookAuthRejectedForTargets(targets, "missing token");
545
+ res.statusCode = 401;
546
+ res.end("unauthorized");
547
+ return true;
548
+ }
549
+ selectedTarget = await resolveGoogleChatWebhookTargetWithAuthOrReject({
550
+ targets,
551
+ res,
552
+ bearer: parsed.addOnBearerToken
553
+ });
554
+ if (!selectedTarget) return true;
555
+ }
556
+ if (!selectedTarget || !parsedEvent) {
557
+ res.statusCode = 401;
558
+ res.end("unauthorized");
559
+ return true;
560
+ }
561
+ const dispatchTarget = selectedTarget;
562
+ dispatchTarget.statusSink?.({ lastInboundAt: Date.now() });
563
+ params.processEvent(parsedEvent, dispatchTarget).catch((err) => {
564
+ dispatchTarget.runtime.error?.(`[${dispatchTarget.account.accountId}] Google Chat webhook failed: ${String(err)}`);
565
+ });
566
+ res.statusCode = 200;
567
+ res.setHeader("Content-Type", "application/json");
568
+ res.end("{}");
569
+ return true;
570
+ }
571
+ });
572
+ };
573
+ }
574
+ //#endregion
575
+ //#region extensions/googlechat/src/monitor-routing.ts
576
+ const webhookTargets = /* @__PURE__ */ new Map();
577
+ const webhookInFlightLimiter = createWebhookInFlightLimiter();
578
+ let processGoogleChatEvent$1 = async () => {};
579
+ function setGoogleChatWebhookEventProcessor(processEvent) {
580
+ processGoogleChatEvent$1 = processEvent;
581
+ }
582
+ const googleChatWebhookRequestHandler = createGoogleChatWebhookRequestHandler({
583
+ webhookTargets,
584
+ webhookInFlightLimiter,
585
+ processEvent: async (event, target) => {
586
+ await processGoogleChatEvent$1(event, target);
587
+ }
588
+ });
589
+ function registerGoogleChatWebhookTarget(target) {
590
+ return registerWebhookTargetWithPluginRoute({
591
+ targetsByPath: webhookTargets,
592
+ target,
593
+ route: {
594
+ auth: "plugin",
595
+ match: "exact",
596
+ pluginId: "googlechat",
597
+ source: "googlechat-webhook",
598
+ accountId: target.account.accountId,
599
+ log: target.runtime.log,
600
+ handler: async (req, res) => {
601
+ if (!await handleGoogleChatWebhookRequest(req, res) && !res.headersSent) {
602
+ res.statusCode = 404;
603
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
604
+ res.end("Not Found");
605
+ }
606
+ }
607
+ }
608
+ }).unregister;
609
+ }
610
+ async function handleGoogleChatWebhookRequest(req, res) {
611
+ return await googleChatWebhookRequestHandler(req, res);
612
+ }
613
+ //#endregion
614
+ //#region extensions/googlechat/src/monitor.ts
615
+ setGoogleChatWebhookEventProcessor(processGoogleChatEvent);
616
+ function logVerbose(core, runtime, message) {
617
+ if (core.logging.shouldLogVerbose()) runtime.log?.(`[googlechat] ${message}`);
618
+ }
619
+ function normalizeAudienceType(value) {
620
+ const normalized = normalizeOptionalLowercaseString(value);
621
+ if (normalized === "app-url" || normalized === "app_url" || normalized === "app") return "app-url";
622
+ if (normalized === "project-number" || normalized === "project_number" || normalized === "project") return "project-number";
623
+ }
624
+ async function processGoogleChatEvent(event, target) {
625
+ if ((event.type ?? event.eventType) !== "MESSAGE") return;
626
+ if (!event.message || !event.space) return;
627
+ await processMessageWithPipeline({
628
+ event,
629
+ account: target.account,
630
+ config: target.config,
631
+ runtime: target.runtime,
632
+ core: target.core,
633
+ statusSink: target.statusSink,
634
+ mediaMaxMb: target.mediaMaxMb
635
+ });
636
+ }
637
+ /**
638
+ * Resolve bot display name with fallback chain:
639
+ * 1. Account config name
640
+ * 2. Agent name from config
641
+ * 3. "OpenClaw" as generic fallback
642
+ */
643
+ function resolveBotDisplayName(params) {
644
+ const { accountName, agentId, config } = params;
645
+ if (accountName?.trim()) return accountName.trim();
646
+ const agent = config.agents?.list?.find((a) => a.id === agentId);
647
+ if (agent?.name?.trim()) return agent.name.trim();
648
+ return "OpenClaw";
649
+ }
650
+ async function processMessageWithPipeline(params) {
651
+ const { event, account, config, runtime, core, statusSink, mediaMaxMb } = params;
652
+ const space = event.space;
653
+ const message = event.message;
654
+ if (!space || !message) return;
655
+ const spaceId = space.name ?? "";
656
+ if (!spaceId) return;
657
+ const isGroup = (space.type ?? "").toUpperCase() !== "DM";
658
+ const sender = message.sender ?? event.user;
659
+ const senderId = sender?.name ?? "";
660
+ const senderName = sender?.displayName ?? "";
661
+ const senderEmail = sender?.email ?? void 0;
662
+ if (!(account.config.allowBots === true)) {
663
+ if (sender?.type?.toUpperCase() === "BOT") {
664
+ logVerbose(core, runtime, `skip bot-authored message (${senderId || "unknown"})`);
665
+ return;
666
+ }
667
+ if (senderId === "users/app") {
668
+ logVerbose(core, runtime, "skip app-authored message");
669
+ return;
670
+ }
671
+ }
672
+ const messageText = (message.argumentText ?? message.text ?? "").trim();
673
+ const attachments = message.attachment ?? [];
674
+ const hasMedia = attachments.length > 0;
675
+ const rawBody = messageText || (hasMedia ? "<media:attachment>" : "");
676
+ if (!rawBody) return;
677
+ const access = await applyGoogleChatInboundAccessPolicy({
678
+ account,
679
+ config,
680
+ core,
681
+ space,
682
+ message,
683
+ isGroup,
684
+ senderId,
685
+ senderName,
686
+ senderEmail,
687
+ rawBody,
688
+ statusSink,
689
+ logVerbose: (message) => logVerbose(core, runtime, message)
690
+ });
691
+ if (!access.ok) return;
692
+ const { commandAuthorized, effectiveWasMentioned, groupSystemPrompt } = access;
693
+ const { route, buildEnvelope } = resolveInboundRouteEnvelopeBuilderWithRuntime({
694
+ cfg: config,
695
+ channel: "googlechat",
696
+ accountId: account.accountId,
697
+ peer: {
698
+ kind: isGroup ? "group" : "direct",
699
+ id: spaceId
700
+ },
701
+ runtime: core.channel,
702
+ sessionStore: config.session?.store
703
+ });
704
+ let mediaPath;
705
+ let mediaType;
706
+ if (attachments.length > 0) {
707
+ const first = attachments[0];
708
+ const attachmentData = await downloadAttachment(first, account, mediaMaxMb, core);
709
+ if (attachmentData) {
710
+ mediaPath = attachmentData.path;
711
+ mediaType = attachmentData.contentType;
712
+ }
713
+ }
714
+ const fromLabel = isGroup ? space.displayName || `space:${spaceId}` : senderName || `user:${senderId}`;
715
+ const { storePath, body } = buildEnvelope({
716
+ channel: "Google Chat",
717
+ from: fromLabel,
718
+ timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
719
+ body: rawBody
720
+ });
721
+ const ctxPayload = core.channel.turn.buildContext({
722
+ channel: "googlechat",
723
+ accountId: route.accountId,
724
+ messageId: message.name,
725
+ messageIdFull: message.name,
726
+ timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
727
+ from: `googlechat:${senderId}`,
728
+ sender: {
729
+ id: senderId,
730
+ name: senderName || void 0,
731
+ username: senderEmail
732
+ },
733
+ conversation: {
734
+ kind: isGroup ? "channel" : "direct",
735
+ id: spaceId,
736
+ label: fromLabel,
737
+ routePeer: {
738
+ kind: isGroup ? "group" : "direct",
739
+ id: spaceId
740
+ }
741
+ },
742
+ route: {
743
+ agentId: route.agentId,
744
+ accountId: route.accountId,
745
+ routeSessionKey: route.sessionKey
746
+ },
747
+ reply: {
748
+ to: `googlechat:${spaceId}`,
749
+ originatingTo: `googlechat:${spaceId}`,
750
+ replyToId: message.thread?.name,
751
+ replyToIdFull: message.thread?.name
752
+ },
753
+ message: {
754
+ body,
755
+ bodyForAgent: rawBody,
756
+ rawBody,
757
+ commandBody: rawBody,
758
+ envelopeFrom: fromLabel
759
+ },
760
+ media: mediaPath || mediaType ? [{
761
+ path: mediaPath,
762
+ url: mediaPath,
763
+ contentType: mediaType
764
+ }] : void 0,
765
+ supplemental: { groupSystemPrompt: isGroup ? groupSystemPrompt : void 0 },
766
+ extra: {
767
+ ChatType: isGroup ? "channel" : "direct",
768
+ WasMentioned: isGroup ? effectiveWasMentioned : void 0,
769
+ CommandAuthorized: commandAuthorized,
770
+ GroupSubject: void 0,
771
+ GroupSpace: isGroup ? space.displayName ?? void 0 : void 0
772
+ }
773
+ });
774
+ let typingIndicator = account.config.typingIndicator ?? "message";
775
+ if (typingIndicator === "reaction") {
776
+ runtime.error?.(`[${account.accountId}] typingIndicator="reaction" requires user OAuth (not supported with service account). Falling back to "message" mode.`);
777
+ typingIndicator = "message";
778
+ }
779
+ let typingMessageName;
780
+ if (typingIndicator === "message") try {
781
+ typingMessageName = (await sendGoogleChatMessage({
782
+ account,
783
+ space: spaceId,
784
+ text: `_${resolveBotDisplayName({
785
+ accountName: account.config.name,
786
+ agentId: route.agentId,
787
+ config
788
+ })} is typing..._`,
789
+ thread: message.thread?.name
790
+ }))?.messageName;
791
+ } catch (err) {
792
+ runtime.error?.(`Failed sending typing message: ${String(err)}`);
793
+ }
794
+ const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
795
+ cfg: config,
796
+ agentId: route.agentId,
797
+ channel: "googlechat",
798
+ accountId: route.accountId
799
+ });
800
+ await core.channel.turn.run({
801
+ channel: "googlechat",
802
+ accountId: route.accountId,
803
+ raw: message,
804
+ adapter: {
805
+ ingest: () => ({
806
+ id: message.name ?? spaceId,
807
+ timestamp: event.eventTime ? Date.parse(event.eventTime) : void 0,
808
+ rawText: rawBody,
809
+ textForAgent: rawBody,
810
+ textForCommands: rawBody,
811
+ raw: message
812
+ }),
813
+ resolveTurn: () => ({
814
+ cfg: config,
815
+ channel: "googlechat",
816
+ accountId: route.accountId,
817
+ agentId: route.agentId,
818
+ routeSessionKey: route.sessionKey,
819
+ storePath,
820
+ ctxPayload,
821
+ recordInboundSession: core.channel.session.recordInboundSession,
822
+ dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
823
+ delivery: {
824
+ deliver: async (payload) => {
825
+ await deliverGoogleChatReply({
826
+ payload,
827
+ account,
828
+ spaceId,
829
+ runtime,
830
+ core,
831
+ config,
832
+ statusSink,
833
+ typingMessageName
834
+ });
835
+ typingMessageName = void 0;
836
+ },
837
+ onError: (err, info) => {
838
+ runtime.error?.(`[${account.accountId}] Google Chat ${info.kind} reply failed: ${String(err)}`);
839
+ }
840
+ },
841
+ dispatcherOptions: replyPipeline,
842
+ replyOptions: { onModelSelected },
843
+ record: { onRecordError: (err) => {
844
+ runtime.error?.(`googlechat: failed updating session meta: ${String(err)}`);
845
+ } }
846
+ })
847
+ }
848
+ });
849
+ }
850
+ async function downloadAttachment(attachment, account, mediaMaxMb, core) {
851
+ const resourceName = attachment.attachmentDataRef?.resourceName;
852
+ if (!resourceName) return null;
853
+ const maxBytes = Math.max(1, mediaMaxMb) * 1024 * 1024;
854
+ const downloaded = await downloadGoogleChatMedia({
855
+ account,
856
+ resourceName,
857
+ maxBytes
858
+ });
859
+ const saved = await core.channel.media.saveMediaBuffer(downloaded.buffer, downloaded.contentType ?? attachment.contentType, "inbound", maxBytes, attachment.contentName);
860
+ return {
861
+ path: saved.path,
862
+ contentType: saved.contentType
863
+ };
864
+ }
865
+ function monitorGoogleChatProvider(options) {
866
+ const core = getGoogleChatRuntime();
867
+ const webhookPath = resolveWebhookPath({
868
+ webhookPath: options.webhookPath,
869
+ webhookUrl: options.webhookUrl,
870
+ defaultPath: "/googlechat"
871
+ });
872
+ if (!webhookPath) {
873
+ options.runtime.error?.(`[${options.account.accountId}] invalid webhook path`);
874
+ return () => {};
875
+ }
876
+ const audienceType = normalizeAudienceType(options.account.config.audienceType);
877
+ const audience = options.account.config.audience?.trim();
878
+ const mediaMaxMb = options.account.config.mediaMaxMb ?? 20;
879
+ warnAppPrincipalMisconfiguration({
880
+ accountId: options.account.accountId,
881
+ audienceType,
882
+ appPrincipal: options.account.config.appPrincipal,
883
+ log: options.runtime.log
884
+ });
885
+ const unregisterTarget = registerGoogleChatWebhookTarget({
886
+ account: options.account,
887
+ config: options.config,
888
+ runtime: options.runtime,
889
+ core,
890
+ path: webhookPath,
891
+ audienceType,
892
+ audience,
893
+ statusSink: options.statusSink,
894
+ mediaMaxMb
895
+ });
896
+ return () => {
897
+ unregisterTarget();
898
+ };
899
+ }
900
+ async function startGoogleChatMonitor(params) {
901
+ return monitorGoogleChatProvider(params);
902
+ }
903
+ function resolveGoogleChatWebhookPath(params) {
904
+ return resolveWebhookPath({
905
+ webhookPath: params.account.config.webhookPath,
906
+ webhookUrl: params.account.config.webhookUrl,
907
+ defaultPath: "/googlechat"
908
+ }) ?? "/googlechat";
909
+ }
910
+ //#endregion
911
+ //#region extensions/googlechat/src/channel.runtime.ts
912
+ const googleChatChannelRuntime = {
913
+ probeGoogleChat,
914
+ sendGoogleChatMessage,
915
+ uploadGoogleChatAttachment,
916
+ resolveGoogleChatWebhookPath,
917
+ startGoogleChatMonitor
918
+ };
919
+ //#endregion
920
+ export { googleChatChannelRuntime };