@kodelyth/googlechat 2026.5.42 → 2026.6.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.
- package/klaw.plugin.json +967 -2
- package/package.json +16 -4
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -1
- package/config-api.ts +0 -2
- package/contract-api.ts +0 -5
- package/doctor-contract-api.ts +0 -1
- package/index.ts +0 -20
- package/runtime-api.ts +0 -55
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/accounts.ts +0 -181
- package/src/actions.test.ts +0 -289
- package/src/actions.ts +0 -227
- package/src/api.ts +0 -316
- package/src/approval-auth.test.ts +0 -24
- package/src/approval-auth.ts +0 -32
- package/src/auth.ts +0 -218
- package/src/channel-config.test.ts +0 -39
- package/src/channel.adapters.ts +0 -340
- package/src/channel.deps.runtime.ts +0 -29
- package/src/channel.runtime.ts +0 -17
- package/src/channel.setup.ts +0 -98
- package/src/channel.test.ts +0 -784
- package/src/channel.ts +0 -277
- package/src/config-schema.test.ts +0 -31
- package/src/config-schema.ts +0 -3
- package/src/doctor-contract.test.ts +0 -75
- package/src/doctor-contract.ts +0 -182
- package/src/doctor.ts +0 -57
- package/src/gateway.ts +0 -63
- package/src/google-auth.runtime.test.ts +0 -543
- package/src/google-auth.runtime.ts +0 -568
- package/src/group-policy.ts +0 -17
- package/src/monitor-access.test.ts +0 -491
- package/src/monitor-access.ts +0 -465
- package/src/monitor-durable.test.ts +0 -39
- package/src/monitor-durable.ts +0 -23
- package/src/monitor-reply-delivery.ts +0 -156
- package/src/monitor-routing.ts +0 -65
- package/src/monitor-types.ts +0 -33
- package/src/monitor-webhook.test.ts +0 -587
- package/src/monitor-webhook.ts +0 -303
- package/src/monitor.reply-delivery.test.ts +0 -144
- package/src/monitor.test.ts +0 -159
- package/src/monitor.ts +0 -527
- package/src/monitor.webhook-routing.test.ts +0 -257
- package/src/runtime.ts +0 -9
- package/src/secret-contract.test.ts +0 -60
- package/src/secret-contract.ts +0 -161
- package/src/setup-core.ts +0 -40
- package/src/setup-surface.ts +0 -243
- package/src/setup.test.ts +0 -619
- package/src/targets.test.ts +0 -453
- package/src/targets.ts +0 -66
- package/src/types.config.ts +0 -3
- package/src/types.ts +0 -73
- package/test-api.ts +0 -2
- package/tsconfig.json +0 -16
package/src/monitor-access.ts
DELETED
|
@@ -1,465 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
channelIngressRoutes,
|
|
3
|
-
createChannelIngressResolver,
|
|
4
|
-
defineStableChannelIngressIdentity,
|
|
5
|
-
} from "klaw/plugin-sdk/channel-ingress-runtime";
|
|
6
|
-
import type { ChannelBotLoopProtectionConfig } from "klaw/plugin-sdk/config-contracts";
|
|
7
|
-
import {
|
|
8
|
-
normalizeLowercaseStringOrEmpty,
|
|
9
|
-
normalizeOptionalString,
|
|
10
|
-
normalizeStringEntries,
|
|
11
|
-
} from "klaw/plugin-sdk/string-coerce-runtime";
|
|
12
|
-
import {
|
|
13
|
-
GROUP_POLICY_BLOCKED_LABEL,
|
|
14
|
-
createChannelPairingController,
|
|
15
|
-
isDangerousNameMatchingEnabled,
|
|
16
|
-
resolveAllowlistProviderRuntimeGroupPolicy,
|
|
17
|
-
resolveDefaultGroupPolicy,
|
|
18
|
-
warnMissingProviderGroupPolicyFallbackOnce,
|
|
19
|
-
type KlawConfig,
|
|
20
|
-
} from "../runtime-api.js";
|
|
21
|
-
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
|
22
|
-
import { sendGoogleChatMessage } from "./api.js";
|
|
23
|
-
import type { GoogleChatCoreRuntime } from "./monitor-types.js";
|
|
24
|
-
import type { GoogleChatAnnotation, GoogleChatMessage, GoogleChatSpace } from "./types.js";
|
|
25
|
-
|
|
26
|
-
function normalizeUserId(raw?: string | null): string {
|
|
27
|
-
const trimmed = normalizeOptionalString(raw) ?? "";
|
|
28
|
-
if (!trimmed) {
|
|
29
|
-
return "";
|
|
30
|
-
}
|
|
31
|
-
return normalizeLowercaseStringOrEmpty(trimmed.replace(/^users\//i, ""));
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const GOOGLECHAT_EMAIL_KIND = "plugin:googlechat-email" as const;
|
|
35
|
-
|
|
36
|
-
function normalizeEntryValue(raw?: string | null): string {
|
|
37
|
-
return normalizeLowercaseStringOrEmpty(raw ?? "");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function normalizeGoogleChatStableEntry(entry: string): string | null {
|
|
41
|
-
const withoutProvider = normalizeEntryValue(entry).replace(
|
|
42
|
-
/^(googlechat|google-chat|gchat):/i,
|
|
43
|
-
"",
|
|
44
|
-
);
|
|
45
|
-
if (!withoutProvider) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
return withoutProvider.startsWith("users/") ? normalizeUserId(withoutProvider) : withoutProvider;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function normalizeGoogleChatEmailEntry(entry: string): string | null {
|
|
52
|
-
const withoutProvider = normalizeEntryValue(entry).replace(
|
|
53
|
-
/^(googlechat|google-chat|gchat):/i,
|
|
54
|
-
"",
|
|
55
|
-
);
|
|
56
|
-
if (withoutProvider.startsWith("users/")) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
const stable = normalizeGoogleChatStableEntry(entry);
|
|
60
|
-
return stable?.includes("@") ? stable : null;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const googleChatIngressIdentity = defineStableChannelIngressIdentity({
|
|
64
|
-
key: "sender-id",
|
|
65
|
-
normalizeEntry: normalizeGoogleChatStableEntry,
|
|
66
|
-
normalizeSubject: normalizeUserId,
|
|
67
|
-
aliases: [
|
|
68
|
-
{
|
|
69
|
-
key: "email",
|
|
70
|
-
kind: GOOGLECHAT_EMAIL_KIND,
|
|
71
|
-
normalizeEntry: normalizeGoogleChatEmailEntry,
|
|
72
|
-
normalizeSubject: normalizeEntryValue,
|
|
73
|
-
dangerous: true,
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
isWildcardEntry: (entry) => normalizeEntryValue(entry) === "*",
|
|
77
|
-
resolveEntryId: ({ entryIndex, fieldKey }) =>
|
|
78
|
-
fieldKey === "stableId"
|
|
79
|
-
? `entry-${entryIndex + 1}:user`
|
|
80
|
-
: `entry-${entryIndex + 1}:${fieldKey}`,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
type GoogleChatGroupEntry = {
|
|
84
|
-
requireMention?: boolean;
|
|
85
|
-
enabled?: boolean;
|
|
86
|
-
botLoopProtection?: ChannelBotLoopProtectionConfig;
|
|
87
|
-
users?: Array<string | number>;
|
|
88
|
-
systemPrompt?: string;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
function resolveGroupConfig(params: {
|
|
92
|
-
groupId: string;
|
|
93
|
-
groupName?: string | null;
|
|
94
|
-
groups?: Record<string, GoogleChatGroupEntry>;
|
|
95
|
-
}) {
|
|
96
|
-
const { groupId, groupName, groups } = params;
|
|
97
|
-
const entries = groups ?? {};
|
|
98
|
-
const keys = Object.keys(entries);
|
|
99
|
-
if (keys.length === 0) {
|
|
100
|
-
return { entry: undefined, allowlistConfigured: false, deprecatedNameMatch: false };
|
|
101
|
-
}
|
|
102
|
-
const entry = entries[groupId];
|
|
103
|
-
const normalizedGroupName = normalizeLowercaseStringOrEmpty(groupName ?? "");
|
|
104
|
-
const deprecatedNameMatch =
|
|
105
|
-
!entry &&
|
|
106
|
-
Boolean(
|
|
107
|
-
groupName &&
|
|
108
|
-
keys.some((key) => {
|
|
109
|
-
const trimmed = key.trim();
|
|
110
|
-
if (!trimmed || trimmed === "*" || /^spaces\//i.test(trimmed)) {
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
return (
|
|
114
|
-
trimmed === groupName || normalizeLowercaseStringOrEmpty(trimmed) === normalizedGroupName
|
|
115
|
-
);
|
|
116
|
-
}),
|
|
117
|
-
);
|
|
118
|
-
const fallback = entries["*"];
|
|
119
|
-
return {
|
|
120
|
-
entry: deprecatedNameMatch ? undefined : (entry ?? fallback),
|
|
121
|
-
allowlistConfigured: true,
|
|
122
|
-
fallback,
|
|
123
|
-
deprecatedNameMatch,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function extractMentionInfo(annotations: GoogleChatAnnotation[], botUser?: string | null) {
|
|
128
|
-
const mentionAnnotations = annotations.filter((entry) => entry.type === "USER_MENTION");
|
|
129
|
-
const hasAnyMention = mentionAnnotations.length > 0;
|
|
130
|
-
const botTargets = new Set(["users/app", botUser?.trim()].filter(Boolean) as string[]);
|
|
131
|
-
const wasMentioned = mentionAnnotations.some((entry) => {
|
|
132
|
-
const userName = entry.userMention?.user?.name;
|
|
133
|
-
if (!userName) {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
if (botTargets.has(userName)) {
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
139
|
-
return normalizeUserId(userName) === "app";
|
|
140
|
-
});
|
|
141
|
-
return { hasAnyMention, wasMentioned };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const warnedDeprecatedUsersEmailAllowFrom = new Set<string>();
|
|
145
|
-
const warnedMutableGroupKeys = new Set<string>();
|
|
146
|
-
|
|
147
|
-
function warnDeprecatedUsersEmailEntries(logVerbose: (message: string) => void, entries: string[]) {
|
|
148
|
-
const deprecated = entries
|
|
149
|
-
.map((v) => normalizeOptionalString(v))
|
|
150
|
-
.filter((v): v is string => Boolean(v))
|
|
151
|
-
.filter((v) => /^users\/.+@.+/i.test(v));
|
|
152
|
-
if (deprecated.length === 0) {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const key = deprecated
|
|
156
|
-
.map((v) => normalizeLowercaseStringOrEmpty(v))
|
|
157
|
-
.toSorted((a, b) => a.localeCompare(b))
|
|
158
|
-
.join(",");
|
|
159
|
-
if (warnedDeprecatedUsersEmailAllowFrom.has(key)) {
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
warnedDeprecatedUsersEmailAllowFrom.add(key);
|
|
163
|
-
logVerbose(
|
|
164
|
-
`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(", ")}`,
|
|
165
|
-
);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function warnMutableGroupKeysConfigured(
|
|
169
|
-
logVerbose: (message: string) => void,
|
|
170
|
-
groups?: Record<string, GoogleChatGroupEntry>,
|
|
171
|
-
) {
|
|
172
|
-
const mutableKeys = Object.keys(groups ?? {})
|
|
173
|
-
.map((key) => key.trim())
|
|
174
|
-
.filter((key) => key && key !== "*" && !/^spaces\//i.test(key));
|
|
175
|
-
if (mutableKeys.length === 0) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const warningKey = mutableKeys
|
|
179
|
-
.map((key) => normalizeLowercaseStringOrEmpty(key))
|
|
180
|
-
.toSorted((a, b) => a.localeCompare(b))
|
|
181
|
-
.join(",");
|
|
182
|
-
if (warnedMutableGroupKeys.has(warningKey)) {
|
|
183
|
-
return;
|
|
184
|
-
}
|
|
185
|
-
warnedMutableGroupKeys.add(warningKey);
|
|
186
|
-
logVerbose(
|
|
187
|
-
`Deprecated Google Chat group key detected: group routing now requires stable space ids (spaces/<spaceId>). Update channels.googlechat.groups keys: ${mutableKeys.join(", ")}`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
export async function applyGoogleChatInboundAccessPolicy(params: {
|
|
192
|
-
account: ResolvedGoogleChatAccount;
|
|
193
|
-
config: KlawConfig;
|
|
194
|
-
core: GoogleChatCoreRuntime;
|
|
195
|
-
space: GoogleChatSpace;
|
|
196
|
-
message: GoogleChatMessage;
|
|
197
|
-
isGroup: boolean;
|
|
198
|
-
senderId: string;
|
|
199
|
-
senderName: string;
|
|
200
|
-
senderEmail?: string;
|
|
201
|
-
rawBody: string;
|
|
202
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
203
|
-
logVerbose: (message: string) => void;
|
|
204
|
-
}): Promise<
|
|
205
|
-
| {
|
|
206
|
-
ok: true;
|
|
207
|
-
commandAuthorized: boolean | undefined;
|
|
208
|
-
effectiveWasMentioned: boolean | undefined;
|
|
209
|
-
groupBotLoopProtection: ChannelBotLoopProtectionConfig | undefined;
|
|
210
|
-
groupSystemPrompt: string | undefined;
|
|
211
|
-
}
|
|
212
|
-
| { ok: false }
|
|
213
|
-
> {
|
|
214
|
-
const {
|
|
215
|
-
account,
|
|
216
|
-
config,
|
|
217
|
-
core,
|
|
218
|
-
space,
|
|
219
|
-
message,
|
|
220
|
-
isGroup,
|
|
221
|
-
senderId,
|
|
222
|
-
senderName,
|
|
223
|
-
senderEmail,
|
|
224
|
-
rawBody,
|
|
225
|
-
statusSink,
|
|
226
|
-
logVerbose,
|
|
227
|
-
} = params;
|
|
228
|
-
const allowNameMatching = isDangerousNameMatchingEnabled(account.config);
|
|
229
|
-
const spaceId = space.name ?? "";
|
|
230
|
-
const pairing = createChannelPairingController({
|
|
231
|
-
core,
|
|
232
|
-
channel: "googlechat",
|
|
233
|
-
accountId: account.accountId,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const defaultGroupPolicy = resolveDefaultGroupPolicy(config);
|
|
237
|
-
const { groupPolicy, providerMissingFallbackApplied } =
|
|
238
|
-
resolveAllowlistProviderRuntimeGroupPolicy({
|
|
239
|
-
providerConfigPresent: config.channels?.googlechat !== undefined,
|
|
240
|
-
groupPolicy: account.config.groupPolicy,
|
|
241
|
-
defaultGroupPolicy,
|
|
242
|
-
});
|
|
243
|
-
warnMissingProviderGroupPolicyFallbackOnce({
|
|
244
|
-
providerMissingFallbackApplied,
|
|
245
|
-
providerKey: "googlechat",
|
|
246
|
-
accountId: account.accountId,
|
|
247
|
-
blockedLabel: GROUP_POLICY_BLOCKED_LABEL.space,
|
|
248
|
-
log: logVerbose,
|
|
249
|
-
});
|
|
250
|
-
warnMutableGroupKeysConfigured(logVerbose, account.config.groups ?? undefined);
|
|
251
|
-
const groupConfigResolved = resolveGroupConfig({
|
|
252
|
-
groupId: spaceId,
|
|
253
|
-
groupName: space.displayName ?? null,
|
|
254
|
-
groups: account.config.groups ?? undefined,
|
|
255
|
-
});
|
|
256
|
-
const groupEntry = groupConfigResolved.entry;
|
|
257
|
-
const groupUsers = groupEntry?.users ?? account.config.groupAllowFrom ?? [];
|
|
258
|
-
let effectiveWasMentioned: boolean | undefined;
|
|
259
|
-
const dmPolicy = account.config.dm?.policy ?? "pairing";
|
|
260
|
-
const rawConfigAllowFrom = normalizeStringEntries(account.config.dm?.allowFrom);
|
|
261
|
-
const shouldComputeAuth = core.channel.commands.shouldComputeCommandAuthorized(rawBody, config);
|
|
262
|
-
const groupActivation = (() => {
|
|
263
|
-
if (!isGroup) {
|
|
264
|
-
return undefined;
|
|
265
|
-
}
|
|
266
|
-
const requireMention = groupEntry?.requireMention ?? account.config.requireMention ?? true;
|
|
267
|
-
const mentionInfo = extractMentionInfo(message.annotations ?? [], account.config.botUser);
|
|
268
|
-
return {
|
|
269
|
-
requireMention,
|
|
270
|
-
allowTextCommands: core.channel.commands.shouldHandleTextCommands({
|
|
271
|
-
cfg: config,
|
|
272
|
-
surface: "googlechat",
|
|
273
|
-
}),
|
|
274
|
-
hasControlCommand: core.channel.text.hasControlCommand(rawBody, config),
|
|
275
|
-
wasMentioned: mentionInfo.wasMentioned,
|
|
276
|
-
hasAnyMention: mentionInfo.hasAnyMention,
|
|
277
|
-
};
|
|
278
|
-
})();
|
|
279
|
-
const command = {
|
|
280
|
-
hasControlCommand: groupActivation?.hasControlCommand ?? shouldComputeAuth,
|
|
281
|
-
groupOwnerAllowFrom: "none" as const,
|
|
282
|
-
};
|
|
283
|
-
const groupAllowFrom = normalizeStringEntries(groupUsers);
|
|
284
|
-
const senderGroupPolicy =
|
|
285
|
-
groupConfigResolved.allowlistConfigured && groupAllowFrom.length === 0
|
|
286
|
-
? groupPolicy
|
|
287
|
-
: groupPolicy === "disabled"
|
|
288
|
-
? "disabled"
|
|
289
|
-
: groupAllowFrom.length > 0
|
|
290
|
-
? "allowlist"
|
|
291
|
-
: "open";
|
|
292
|
-
const route = channelIngressRoutes(
|
|
293
|
-
isGroup &&
|
|
294
|
-
groupPolicy !== "disabled" &&
|
|
295
|
-
groupEntry?.enabled === false && {
|
|
296
|
-
id: "googlechat:space",
|
|
297
|
-
enabled: false,
|
|
298
|
-
matched: true,
|
|
299
|
-
matchId: "googlechat-space",
|
|
300
|
-
blockReason: "route_disabled",
|
|
301
|
-
},
|
|
302
|
-
isGroup &&
|
|
303
|
-
groupPolicy === "allowlist" &&
|
|
304
|
-
groupEntry?.enabled !== false &&
|
|
305
|
-
!groupConfigResolved.allowlistConfigured && {
|
|
306
|
-
id: "googlechat:space",
|
|
307
|
-
allowed: false,
|
|
308
|
-
blockReason: "empty_allowlist",
|
|
309
|
-
},
|
|
310
|
-
isGroup &&
|
|
311
|
-
groupPolicy === "allowlist" &&
|
|
312
|
-
groupEntry?.enabled !== false &&
|
|
313
|
-
groupConfigResolved.allowlistConfigured && {
|
|
314
|
-
id: "googlechat:space",
|
|
315
|
-
senderPolicy: "deny-when-empty" as const,
|
|
316
|
-
...(groupEntry ? { senderAllowFromSource: "effective-group" as const } : {}),
|
|
317
|
-
allowed: Boolean(groupEntry),
|
|
318
|
-
matchId: "googlechat-space",
|
|
319
|
-
blockReason: groupEntry ? "sender_empty_allowlist" : "route_not_allowlisted",
|
|
320
|
-
},
|
|
321
|
-
);
|
|
322
|
-
const resolvedAccess = await createChannelIngressResolver({
|
|
323
|
-
channelId: "googlechat",
|
|
324
|
-
accountId: account.accountId,
|
|
325
|
-
identity: googleChatIngressIdentity,
|
|
326
|
-
cfg: config,
|
|
327
|
-
readStoreAllowFrom: pairing.readAllowFromStore,
|
|
328
|
-
}).message({
|
|
329
|
-
subject: {
|
|
330
|
-
stableId: senderId,
|
|
331
|
-
aliases: { email: senderEmail },
|
|
332
|
-
},
|
|
333
|
-
conversation: {
|
|
334
|
-
kind: isGroup ? "group" : "direct",
|
|
335
|
-
id: spaceId,
|
|
336
|
-
},
|
|
337
|
-
route,
|
|
338
|
-
allowFrom: rawConfigAllowFrom,
|
|
339
|
-
groupAllowFrom,
|
|
340
|
-
dmPolicy,
|
|
341
|
-
groupPolicy: senderGroupPolicy,
|
|
342
|
-
policy: {
|
|
343
|
-
groupAllowFromFallbackToAllowFrom: false,
|
|
344
|
-
mutableIdentifierMatching: allowNameMatching ? "enabled" : "disabled",
|
|
345
|
-
...(groupActivation
|
|
346
|
-
? {
|
|
347
|
-
activation: {
|
|
348
|
-
requireMention: groupActivation.requireMention,
|
|
349
|
-
allowTextCommands: groupActivation.allowTextCommands,
|
|
350
|
-
},
|
|
351
|
-
}
|
|
352
|
-
: {}),
|
|
353
|
-
},
|
|
354
|
-
...(groupActivation == null
|
|
355
|
-
? {}
|
|
356
|
-
: {
|
|
357
|
-
mentionFacts: {
|
|
358
|
-
canDetectMention: true,
|
|
359
|
-
wasMentioned: groupActivation.wasMentioned,
|
|
360
|
-
hasAnyMention: groupActivation.hasAnyMention,
|
|
361
|
-
implicitMentionKinds: [],
|
|
362
|
-
},
|
|
363
|
-
}),
|
|
364
|
-
command,
|
|
365
|
-
});
|
|
366
|
-
const senderAccess = resolvedAccess.senderAccess;
|
|
367
|
-
const commandAuthorized = resolvedAccess.commandAccess.requested
|
|
368
|
-
? resolvedAccess.commandAccess.authorized
|
|
369
|
-
: undefined;
|
|
370
|
-
|
|
371
|
-
if (isGroup) {
|
|
372
|
-
if (groupConfigResolved.deprecatedNameMatch) {
|
|
373
|
-
logVerbose(`drop group message (deprecated mutable group key matched, space=${spaceId})`);
|
|
374
|
-
return { ok: false };
|
|
375
|
-
}
|
|
376
|
-
const routeBlockReason = resolvedAccess.routeAccess.reason;
|
|
377
|
-
if (routeBlockReason && routeBlockReason !== "sender_empty_allowlist") {
|
|
378
|
-
if (routeBlockReason === "empty_allowlist") {
|
|
379
|
-
logVerbose(`drop group message (groupPolicy=allowlist, no allowlist, space=${spaceId})`);
|
|
380
|
-
} else if (routeBlockReason === "route_not_allowlisted") {
|
|
381
|
-
logVerbose(`drop group message (not allowlisted, space=${spaceId})`);
|
|
382
|
-
} else if (routeBlockReason === "route_disabled") {
|
|
383
|
-
logVerbose(`drop group message (space disabled, space=${spaceId})`);
|
|
384
|
-
}
|
|
385
|
-
return { ok: false };
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (senderAccess.effectiveGroupAllowFrom.length > 0 && senderAccess.decision !== "allow") {
|
|
389
|
-
warnDeprecatedUsersEmailEntries(logVerbose, senderAccess.effectiveGroupAllowFrom);
|
|
390
|
-
logVerbose(`drop group message (sender not allowed, ${senderId})`);
|
|
391
|
-
return { ok: false };
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const effectiveAllowFrom = senderAccess.effectiveAllowFrom;
|
|
396
|
-
warnDeprecatedUsersEmailEntries(logVerbose, effectiveAllowFrom);
|
|
397
|
-
|
|
398
|
-
if (isGroup && resolvedAccess.activationAccess.ran) {
|
|
399
|
-
effectiveWasMentioned = resolvedAccess.activationAccess.effectiveWasMentioned;
|
|
400
|
-
if (resolvedAccess.activationAccess.shouldSkip) {
|
|
401
|
-
logVerbose(`drop group message (mention required, space=${spaceId})`);
|
|
402
|
-
return { ok: false };
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
if (isGroup && senderAccess.decision !== "allow") {
|
|
407
|
-
const reason =
|
|
408
|
-
resolvedAccess.ingress.reasonCode === "route_sender_empty"
|
|
409
|
-
? "groupPolicy=allowlist (empty allowlist)"
|
|
410
|
-
: senderAccess.reasonCode;
|
|
411
|
-
logVerbose(`drop group message (sender policy blocked, reason=${reason}, space=${spaceId})`);
|
|
412
|
-
return { ok: false };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
if (!isGroup) {
|
|
416
|
-
if (account.config.dm?.enabled === false) {
|
|
417
|
-
logVerbose(`Blocked Google Chat DM from ${senderId} (dmPolicy=disabled)`);
|
|
418
|
-
return { ok: false };
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (senderAccess.decision !== "allow") {
|
|
422
|
-
if (senderAccess.decision === "pairing") {
|
|
423
|
-
await pairing.issueChallenge({
|
|
424
|
-
senderId,
|
|
425
|
-
senderIdLine: `Your Google Chat user id: ${senderId}`,
|
|
426
|
-
meta: { name: senderName || undefined, email: senderEmail },
|
|
427
|
-
onCreated: () => {
|
|
428
|
-
logVerbose(`googlechat pairing request sender=${senderId}`);
|
|
429
|
-
},
|
|
430
|
-
sendPairingReply: async (text) => {
|
|
431
|
-
await sendGoogleChatMessage({
|
|
432
|
-
account,
|
|
433
|
-
space: spaceId,
|
|
434
|
-
text,
|
|
435
|
-
});
|
|
436
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
437
|
-
},
|
|
438
|
-
onReplyError: (err) => {
|
|
439
|
-
logVerbose(`pairing reply failed for ${senderId}: ${String(err)}`);
|
|
440
|
-
},
|
|
441
|
-
});
|
|
442
|
-
} else {
|
|
443
|
-
logVerbose(`Blocked unauthorized Google Chat sender ${senderId} (dmPolicy=${dmPolicy})`);
|
|
444
|
-
}
|
|
445
|
-
return { ok: false };
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (
|
|
450
|
-
isGroup &&
|
|
451
|
-
core.channel.commands.isControlCommandMessage(rawBody, config) &&
|
|
452
|
-
commandAuthorized !== true
|
|
453
|
-
) {
|
|
454
|
-
logVerbose(`googlechat: drop control command from ${senderId}`);
|
|
455
|
-
return { ok: false };
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
return {
|
|
459
|
-
ok: true,
|
|
460
|
-
commandAuthorized,
|
|
461
|
-
effectiveWasMentioned,
|
|
462
|
-
groupBotLoopProtection: groupEntry?.botLoopProtection,
|
|
463
|
-
groupSystemPrompt: normalizeOptionalString(groupEntry?.systemPrompt),
|
|
464
|
-
};
|
|
465
|
-
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { resolveGoogleChatDurableReplyOptions } from "./monitor-durable.js";
|
|
3
|
-
|
|
4
|
-
describe("resolveGoogleChatDurableReplyOptions", () => {
|
|
5
|
-
it("enables durable final delivery when no typing preview is active", () => {
|
|
6
|
-
expect(
|
|
7
|
-
resolveGoogleChatDurableReplyOptions({
|
|
8
|
-
payload: { text: "hello", replyToId: "thread-1" },
|
|
9
|
-
infoKind: "final",
|
|
10
|
-
spaceId: "spaces/AAA",
|
|
11
|
-
}),
|
|
12
|
-
).toEqual({
|
|
13
|
-
to: "spaces/AAA",
|
|
14
|
-
replyToId: "thread-1",
|
|
15
|
-
threadId: "thread-1",
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("keeps typing preview delivery on the legacy edit path", () => {
|
|
20
|
-
expect(
|
|
21
|
-
resolveGoogleChatDurableReplyOptions({
|
|
22
|
-
payload: { text: "hello" },
|
|
23
|
-
infoKind: "final",
|
|
24
|
-
spaceId: "spaces/AAA",
|
|
25
|
-
typingMessageName: "spaces/AAA/messages/typing",
|
|
26
|
-
}),
|
|
27
|
-
).toBe(false);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("does not durable-deliver non-final chunks", () => {
|
|
31
|
-
expect(
|
|
32
|
-
resolveGoogleChatDurableReplyOptions({
|
|
33
|
-
payload: { text: "hello" },
|
|
34
|
-
infoKind: "block",
|
|
35
|
-
spaceId: "spaces/AAA",
|
|
36
|
-
}),
|
|
37
|
-
).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
});
|
package/src/monitor-durable.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import type { ReplyPayload } from "klaw/plugin-sdk/reply-runtime";
|
|
2
|
-
|
|
3
|
-
export type GoogleChatDurableReplyOptions = {
|
|
4
|
-
to: string;
|
|
5
|
-
replyToId?: string;
|
|
6
|
-
threadId?: string;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export function resolveGoogleChatDurableReplyOptions(params: {
|
|
10
|
-
payload: ReplyPayload;
|
|
11
|
-
infoKind: string;
|
|
12
|
-
spaceId: string;
|
|
13
|
-
typingMessageName?: string;
|
|
14
|
-
}): GoogleChatDurableReplyOptions | false {
|
|
15
|
-
if (params.infoKind !== "final" || params.typingMessageName) {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
const threadId = params.payload.replyToId?.trim() || undefined;
|
|
19
|
-
return {
|
|
20
|
-
to: params.spaceId,
|
|
21
|
-
...(threadId ? { replyToId: threadId, threadId } : {}),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
deliverTextOrMediaReply,
|
|
3
|
-
resolveSendableOutboundReplyParts,
|
|
4
|
-
} from "klaw/plugin-sdk/reply-payload";
|
|
5
|
-
import type { KlawConfig } from "../runtime-api.js";
|
|
6
|
-
import type { ResolvedGoogleChatAccount } from "./accounts.js";
|
|
7
|
-
import {
|
|
8
|
-
deleteGoogleChatMessage,
|
|
9
|
-
sendGoogleChatMessage,
|
|
10
|
-
updateGoogleChatMessage,
|
|
11
|
-
uploadGoogleChatAttachment,
|
|
12
|
-
} from "./api.js";
|
|
13
|
-
import type { GoogleChatCoreRuntime, GoogleChatRuntimeEnv } from "./monitor-types.js";
|
|
14
|
-
|
|
15
|
-
export async function deliverGoogleChatReply(params: {
|
|
16
|
-
payload: { text?: string; mediaUrls?: string[]; mediaUrl?: string; replyToId?: string };
|
|
17
|
-
account: ResolvedGoogleChatAccount;
|
|
18
|
-
spaceId: string;
|
|
19
|
-
runtime: GoogleChatRuntimeEnv;
|
|
20
|
-
core: GoogleChatCoreRuntime;
|
|
21
|
-
config: KlawConfig;
|
|
22
|
-
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
|
|
23
|
-
typingMessageName?: string;
|
|
24
|
-
}): Promise<void> {
|
|
25
|
-
const { payload, account, spaceId, runtime, core, config, statusSink } = params;
|
|
26
|
-
// Clear this whenever the typing message is deleted or unavailable; otherwise
|
|
27
|
-
// text delivery can keep retrying a dead message and drop content.
|
|
28
|
-
let typingMessageName = params.typingMessageName;
|
|
29
|
-
const reply = resolveSendableOutboundReplyParts(payload);
|
|
30
|
-
const mediaCount = reply.mediaCount;
|
|
31
|
-
const hasMedia = reply.hasMedia;
|
|
32
|
-
const text = reply.text;
|
|
33
|
-
let firstTextChunk = true;
|
|
34
|
-
let suppressCaption = false;
|
|
35
|
-
|
|
36
|
-
if (hasMedia && typingMessageName) {
|
|
37
|
-
try {
|
|
38
|
-
await deleteGoogleChatMessage({
|
|
39
|
-
account,
|
|
40
|
-
messageName: typingMessageName,
|
|
41
|
-
});
|
|
42
|
-
typingMessageName = undefined;
|
|
43
|
-
} catch (err) {
|
|
44
|
-
runtime.error?.(`Google Chat typing cleanup failed: ${String(err)}`);
|
|
45
|
-
if (typingMessageName) {
|
|
46
|
-
const fallbackText = reply.hasText
|
|
47
|
-
? text
|
|
48
|
-
: mediaCount > 1
|
|
49
|
-
? "Sent attachments."
|
|
50
|
-
: "Sent attachment.";
|
|
51
|
-
try {
|
|
52
|
-
await updateGoogleChatMessage({
|
|
53
|
-
account,
|
|
54
|
-
messageName: typingMessageName,
|
|
55
|
-
text: fallbackText,
|
|
56
|
-
});
|
|
57
|
-
suppressCaption = Boolean(text.trim());
|
|
58
|
-
} catch (updateErr) {
|
|
59
|
-
runtime.error?.(`Google Chat typing update failed: ${String(updateErr)}`);
|
|
60
|
-
typingMessageName = undefined;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const chunkLimit = account.config.textChunkLimit ?? 4000;
|
|
67
|
-
const chunkMode = core.channel.text.resolveChunkMode(config, "googlechat", account.accountId);
|
|
68
|
-
const sendTextMessage = async (chunk: string) => {
|
|
69
|
-
await sendGoogleChatMessage({
|
|
70
|
-
account,
|
|
71
|
-
space: spaceId,
|
|
72
|
-
text: chunk,
|
|
73
|
-
thread: payload.replyToId,
|
|
74
|
-
});
|
|
75
|
-
};
|
|
76
|
-
await deliverTextOrMediaReply({
|
|
77
|
-
payload,
|
|
78
|
-
text: suppressCaption ? "" : reply.text,
|
|
79
|
-
chunkText: (value) => core.channel.text.chunkMarkdownTextWithMode(value, chunkLimit, chunkMode),
|
|
80
|
-
sendText: async (chunk) => {
|
|
81
|
-
try {
|
|
82
|
-
if (firstTextChunk && typingMessageName) {
|
|
83
|
-
await updateGoogleChatMessage({
|
|
84
|
-
account,
|
|
85
|
-
messageName: typingMessageName,
|
|
86
|
-
text: chunk,
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
await sendTextMessage(chunk);
|
|
90
|
-
}
|
|
91
|
-
firstTextChunk = false;
|
|
92
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
93
|
-
} catch (err) {
|
|
94
|
-
runtime.error?.(`Google Chat message send failed: ${String(err)}`);
|
|
95
|
-
if (firstTextChunk && typingMessageName) {
|
|
96
|
-
typingMessageName = undefined;
|
|
97
|
-
try {
|
|
98
|
-
await sendTextMessage(chunk);
|
|
99
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
100
|
-
} catch (fallbackErr) {
|
|
101
|
-
runtime.error?.(`Google Chat message fallback send failed: ${String(fallbackErr)}`);
|
|
102
|
-
} finally {
|
|
103
|
-
firstTextChunk = false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
sendMedia: async ({ mediaUrl, caption }) => {
|
|
109
|
-
try {
|
|
110
|
-
const loaded = await core.channel.media.readRemoteMediaBuffer({
|
|
111
|
-
url: mediaUrl,
|
|
112
|
-
maxBytes: (account.config.mediaMaxMb ?? 20) * 1024 * 1024,
|
|
113
|
-
});
|
|
114
|
-
const upload = await uploadAttachmentForReply({
|
|
115
|
-
account,
|
|
116
|
-
spaceId,
|
|
117
|
-
buffer: loaded.buffer,
|
|
118
|
-
contentType: loaded.contentType,
|
|
119
|
-
filename: loaded.fileName ?? "attachment",
|
|
120
|
-
});
|
|
121
|
-
if (!upload.attachmentUploadToken) {
|
|
122
|
-
throw new Error("missing attachment upload token");
|
|
123
|
-
}
|
|
124
|
-
await sendGoogleChatMessage({
|
|
125
|
-
account,
|
|
126
|
-
space: spaceId,
|
|
127
|
-
text: caption,
|
|
128
|
-
thread: payload.replyToId,
|
|
129
|
-
attachments: [
|
|
130
|
-
{ attachmentUploadToken: upload.attachmentUploadToken, contentName: loaded.fileName },
|
|
131
|
-
],
|
|
132
|
-
});
|
|
133
|
-
statusSink?.({ lastOutboundAt: Date.now() });
|
|
134
|
-
} catch (err) {
|
|
135
|
-
runtime.error?.(`Google Chat attachment send failed: ${String(err)}`);
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async function uploadAttachmentForReply(params: {
|
|
142
|
-
account: ResolvedGoogleChatAccount;
|
|
143
|
-
spaceId: string;
|
|
144
|
-
buffer: Buffer;
|
|
145
|
-
contentType?: string;
|
|
146
|
-
filename: string;
|
|
147
|
-
}) {
|
|
148
|
-
const { account, spaceId, buffer, contentType, filename } = params;
|
|
149
|
-
return await uploadGoogleChatAttachment({
|
|
150
|
-
account,
|
|
151
|
-
space: spaceId,
|
|
152
|
-
filename,
|
|
153
|
-
buffer,
|
|
154
|
-
contentType,
|
|
155
|
-
});
|
|
156
|
-
}
|