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