@actagent/googlechat 2026.6.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.
- package/README.md +11 -0
- package/actagent.plugin.json +17 -0
- package/api.ts +4 -0
- package/channel-config-api.ts +2 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +3 -0
- package/contract-api.ts +6 -0
- package/directory-contract-api.ts +7 -0
- package/doctor-contract-api.ts +2 -0
- package/index.ts +21 -0
- package/npm-shrinkwrap.json +314 -0
- package/package.json +88 -0
- package/runtime-api.ts +61 -0
- package/secret-contract-api.ts +6 -0
- package/setup-entry.ts +14 -0
- package/setup-plugin-api.ts +3 -0
- package/src/accounts.ts +185 -0
- package/src/actions.test.ts +312 -0
- package/src/actions.ts +228 -0
- package/src/api.ts +346 -0
- package/src/approval-auth.test.ts +25 -0
- package/src/approval-auth.ts +38 -0
- package/src/approval-card-actions.test.ts +113 -0
- package/src/approval-card-actions.ts +307 -0
- package/src/approval-card-click.test.ts +279 -0
- package/src/approval-card-click.ts +94 -0
- package/src/approval-handler.runtime.test.ts +388 -0
- package/src/approval-handler.runtime.ts +413 -0
- package/src/approval-native.test.ts +399 -0
- package/src/approval-native.ts +246 -0
- package/src/auth.ts +219 -0
- package/src/channel-base.ts +123 -0
- package/src/channel-config.test.ts +174 -0
- package/src/channel.adapters.ts +363 -0
- package/src/channel.deps.runtime.ts +30 -0
- package/src/channel.runtime.ts +18 -0
- package/src/channel.setup.ts +7 -0
- package/src/channel.test.ts +845 -0
- package/src/channel.ts +214 -0
- package/src/config-schema.test.ts +32 -0
- package/src/config-schema.ts +4 -0
- package/src/doctor-contract.test.ts +76 -0
- package/src/doctor-contract.ts +181 -0
- package/src/doctor.ts +58 -0
- package/src/gateway.ts +84 -0
- package/src/google-auth.runtime.test.ts +571 -0
- package/src/google-auth.runtime.ts +570 -0
- package/src/group-policy.ts +18 -0
- package/src/monitor-access.test.ts +492 -0
- package/src/monitor-access.ts +466 -0
- package/src/monitor-durable.test.ts +40 -0
- package/src/monitor-durable.ts +24 -0
- package/src/monitor-reply-delivery.ts +162 -0
- package/src/monitor-routing.ts +66 -0
- package/src/monitor-types.ts +34 -0
- package/src/monitor-webhook.test.ts +670 -0
- package/src/monitor-webhook.ts +361 -0
- package/src/monitor.reply-delivery.test.ts +145 -0
- package/src/monitor.test.ts +389 -0
- package/src/monitor.ts +530 -0
- package/src/monitor.webhook-routing.test.ts +258 -0
- package/src/runtime.ts +10 -0
- package/src/secret-contract.test.ts +61 -0
- package/src/secret-contract.ts +162 -0
- package/src/setup-core.ts +41 -0
- package/src/setup-surface.ts +244 -0
- package/src/setup.test.ts +620 -0
- package/src/targets.test.ts +562 -0
- package/src/targets.ts +67 -0
- package/src/types.config.ts +4 -0
- package/src/types.ts +139 -0
- package/test-api.ts +3 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
clearGoogleChatApprovalCardBindingsForTest,
|
|
4
|
+
registerGoogleChatManualApprovalFollowupSuppression,
|
|
5
|
+
registerGoogleChatApprovalCardBinding,
|
|
6
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupPayload,
|
|
7
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText,
|
|
8
|
+
} from "./approval-card-actions.js";
|
|
9
|
+
|
|
10
|
+
const approvalId = "12345678-1234-1234-1234-123456789012";
|
|
11
|
+
type TestExecApprovalDecision = "allow-once" | "allow-always" | "deny";
|
|
12
|
+
let tokenCounter = 0;
|
|
13
|
+
|
|
14
|
+
function registerExecApprovalCard(overrides?: {
|
|
15
|
+
approvalId?: string;
|
|
16
|
+
expiresAtMs?: number;
|
|
17
|
+
allowedDecisions?: readonly TestExecApprovalDecision[];
|
|
18
|
+
}): void {
|
|
19
|
+
registerGoogleChatApprovalCardBinding({
|
|
20
|
+
token: `token-${tokenCounter++}`,
|
|
21
|
+
accountId: "default",
|
|
22
|
+
approvalId: overrides?.approvalId ?? approvalId,
|
|
23
|
+
approvalKind: "exec",
|
|
24
|
+
decision: "allow-once",
|
|
25
|
+
allowedDecisions: overrides?.allowedDecisions ?? ["allow-once", "deny"],
|
|
26
|
+
spaceName: "spaces/AAA",
|
|
27
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
28
|
+
expiresAtMs: overrides?.expiresAtMs ?? Date.now() + 60_000,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("Google Chat approval card action registry", () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
clearGoogleChatApprovalCardBindingsForTest();
|
|
35
|
+
tokenCounter = 0;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("suppresses manual exec approval follow-up text for an active native card", () => {
|
|
39
|
+
registerExecApprovalCard();
|
|
40
|
+
|
|
41
|
+
expect(
|
|
42
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText(
|
|
43
|
+
`I need approval.\nReply with:\n/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
expect(
|
|
47
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText(
|
|
48
|
+
`Run this if needed: \`/approve ${approvalId} deny\``,
|
|
49
|
+
),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("suppresses manual exec approval follow-up text after native delivery before token binding", () => {
|
|
54
|
+
registerGoogleChatManualApprovalFollowupSuppression({
|
|
55
|
+
approvalId,
|
|
56
|
+
approvalKind: "exec",
|
|
57
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
58
|
+
expiresAtMs: Date.now() + 60_000,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText(
|
|
63
|
+
`Please reply with:\n/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
64
|
+
),
|
|
65
|
+
).toBe(true);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("keeps unrelated, expired, and non-sendable approval text visible", () => {
|
|
69
|
+
registerExecApprovalCard({ expiresAtMs: Date.now() - 1 });
|
|
70
|
+
expect(
|
|
71
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText(
|
|
72
|
+
`/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
73
|
+
),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
|
|
76
|
+
clearGoogleChatApprovalCardBindingsForTest();
|
|
77
|
+
registerExecApprovalCard();
|
|
78
|
+
expect(
|
|
79
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText("/approve deadbeef allow-once"),
|
|
80
|
+
).toBe(false);
|
|
81
|
+
expect(
|
|
82
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupText(`/approve ${approvalId} nope`),
|
|
83
|
+
).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("suppresses only text-only manual approval follow-up payloads", () => {
|
|
87
|
+
registerExecApprovalCard();
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupPayload({
|
|
91
|
+
text: `/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
92
|
+
}),
|
|
93
|
+
).toBe(true);
|
|
94
|
+
expect(
|
|
95
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupPayload({
|
|
96
|
+
text: `/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
97
|
+
mediaUrl: "https://example.test/image.png",
|
|
98
|
+
}),
|
|
99
|
+
).toBe(false);
|
|
100
|
+
expect(
|
|
101
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupPayload({
|
|
102
|
+
text: `/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
103
|
+
channelData: { execApproval: { approvalId } },
|
|
104
|
+
}),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
expect(
|
|
107
|
+
shouldSuppressGoogleChatManualExecApprovalFollowupPayload({
|
|
108
|
+
text: `/approve ${approvalId.slice(0, 8)} allow-once`,
|
|
109
|
+
presentation: { blocks: [] },
|
|
110
|
+
}),
|
|
111
|
+
).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { ExecApprovalDecision } from "actagent/plugin-sdk/approval-runtime";
|
|
3
|
+
import { normalizeOptionalString } from "actagent/plugin-sdk/string-coerce-runtime";
|
|
4
|
+
import type { GoogleChatActionParameter, GoogleChatEvent } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const GOOGLECHAT_APPROVAL_ACTION = "actagent.approval";
|
|
7
|
+
const GOOGLECHAT_APPROVAL_ACTION_PARAM = "actagent_action";
|
|
8
|
+
const GOOGLECHAT_APPROVAL_TOKEN_PARAM = "token";
|
|
9
|
+
const GOOGLECHAT_APPROVAL_ACTION_VALUE = "approval";
|
|
10
|
+
const MANUAL_EXEC_APPROVAL_COMMAND_RE =
|
|
11
|
+
/(?:^|[\s`])\/approve[ \t]+([^ \t\r\n`|]+)[ \t]+(allow-once|allow-always|deny)(?=$|[\s`|.,;:!?])/giu;
|
|
12
|
+
|
|
13
|
+
export type GoogleChatApprovalCardBinding = {
|
|
14
|
+
token: string;
|
|
15
|
+
accountId: string;
|
|
16
|
+
approvalId: string;
|
|
17
|
+
approvalKind: "exec" | "plugin";
|
|
18
|
+
decision: ExecApprovalDecision;
|
|
19
|
+
allowedDecisions: readonly ExecApprovalDecision[];
|
|
20
|
+
spaceName: string;
|
|
21
|
+
messageName: string;
|
|
22
|
+
threadName?: string | null;
|
|
23
|
+
expiresAtMs: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const approvalCardBindings = new Map<string, GoogleChatApprovalCardBinding>();
|
|
27
|
+
const approvalCardResolvingTokens = new Set<string>();
|
|
28
|
+
|
|
29
|
+
type GoogleChatManualApprovalSuppressionPayload = {
|
|
30
|
+
text?: string;
|
|
31
|
+
mediaUrl?: string;
|
|
32
|
+
mediaUrls?: string[];
|
|
33
|
+
presentation?: unknown;
|
|
34
|
+
interactive?: unknown;
|
|
35
|
+
channelData?: unknown;
|
|
36
|
+
btw?: unknown;
|
|
37
|
+
spokenText?: unknown;
|
|
38
|
+
ttsSupplement?: unknown;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type GoogleChatManualApprovalFollowupSuppression = {
|
|
42
|
+
approvalId: string;
|
|
43
|
+
approvalKind: "exec" | "plugin";
|
|
44
|
+
allowedDecisions: readonly ExecApprovalDecision[];
|
|
45
|
+
expiresAtMs: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type GoogleChatApprovalCardClaim =
|
|
49
|
+
| { kind: "claimed"; binding: GoogleChatApprovalCardBinding }
|
|
50
|
+
| { kind: "missing" }
|
|
51
|
+
| { kind: "in-flight" };
|
|
52
|
+
|
|
53
|
+
const manualApprovalFollowupSuppressions = new Map<
|
|
54
|
+
string,
|
|
55
|
+
GoogleChatManualApprovalFollowupSuppression
|
|
56
|
+
>();
|
|
57
|
+
|
|
58
|
+
export function createGoogleChatApprovalToken(): string {
|
|
59
|
+
return crypto.randomBytes(18).toString("base64url");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildGoogleChatApprovalActionParameters(
|
|
63
|
+
token: string,
|
|
64
|
+
): GoogleChatActionParameter[] {
|
|
65
|
+
return [
|
|
66
|
+
{ key: GOOGLECHAT_APPROVAL_ACTION_PARAM, value: GOOGLECHAT_APPROVAL_ACTION_VALUE },
|
|
67
|
+
{ key: GOOGLECHAT_APPROVAL_TOKEN_PARAM, value: token },
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectEventParameters(event: GoogleChatEvent): Record<string, string> {
|
|
72
|
+
const params: Record<string, string> = {};
|
|
73
|
+
for (const [key, value] of Object.entries(event.common?.parameters ?? {})) {
|
|
74
|
+
if (typeof value === "string") {
|
|
75
|
+
params[key] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const [key, value] of Object.entries(event.commonEventObject?.parameters ?? {})) {
|
|
79
|
+
if (typeof value === "string") {
|
|
80
|
+
params[key] = value;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const item of event.action?.parameters ?? []) {
|
|
84
|
+
if (typeof item.key === "string" && typeof item.value === "string") {
|
|
85
|
+
params[item.key] = item.value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return params;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readGoogleChatApprovalActionToken(event: GoogleChatEvent): string | null {
|
|
92
|
+
const params = collectEventParameters(event);
|
|
93
|
+
if (params[GOOGLECHAT_APPROVAL_ACTION_PARAM] !== GOOGLECHAT_APPROVAL_ACTION_VALUE) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const actionName =
|
|
97
|
+
normalizeOptionalString(event.action?.actionMethodName) ??
|
|
98
|
+
normalizeOptionalString(event.common?.invokedFunction) ??
|
|
99
|
+
normalizeOptionalString(event.commonEventObject?.invokedFunction);
|
|
100
|
+
if (
|
|
101
|
+
actionName &&
|
|
102
|
+
actionName !== GOOGLECHAT_APPROVAL_ACTION &&
|
|
103
|
+
!actionName.startsWith("https://")
|
|
104
|
+
) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
return normalizeOptionalString(params[GOOGLECHAT_APPROVAL_TOKEN_PARAM]) ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function registerGoogleChatApprovalCardBinding(
|
|
111
|
+
binding: GoogleChatApprovalCardBinding,
|
|
112
|
+
): boolean {
|
|
113
|
+
if (binding.expiresAtMs <= Date.now()) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
approvalCardBindings.set(binding.token, binding);
|
|
117
|
+
registerGoogleChatManualApprovalFollowupSuppression({
|
|
118
|
+
approvalId: binding.approvalId,
|
|
119
|
+
approvalKind: binding.approvalKind,
|
|
120
|
+
allowedDecisions: binding.allowedDecisions,
|
|
121
|
+
expiresAtMs: binding.expiresAtMs,
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getGoogleChatApprovalCardBinding(
|
|
127
|
+
token: string,
|
|
128
|
+
): GoogleChatApprovalCardBinding | null {
|
|
129
|
+
const binding = approvalCardBindings.get(token);
|
|
130
|
+
if (!binding) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
if (binding.expiresAtMs <= Date.now()) {
|
|
134
|
+
approvalCardBindings.delete(token);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
return binding;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function normalizeApprovalRef(value: string): string | null {
|
|
141
|
+
const normalized = value.trim().toLowerCase();
|
|
142
|
+
return normalized ? normalized : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function manualApprovalFollowupSuppressionKey(approvalId: string): string | null {
|
|
146
|
+
return normalizeApprovalRef(approvalId);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function registerGoogleChatManualApprovalFollowupSuppression(
|
|
150
|
+
suppression: GoogleChatManualApprovalFollowupSuppression,
|
|
151
|
+
): boolean {
|
|
152
|
+
if (suppression.expiresAtMs <= Date.now()) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const key = manualApprovalFollowupSuppressionKey(suppression.approvalId);
|
|
156
|
+
if (!key) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
manualApprovalFollowupSuppressions.set(key, suppression);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function unregisterGoogleChatManualApprovalFollowupSuppression(approvalId: string): void {
|
|
164
|
+
const key = manualApprovalFollowupSuppressionKey(approvalId);
|
|
165
|
+
if (key) {
|
|
166
|
+
manualApprovalFollowupSuppressions.delete(key);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function approvalRefMatches(bindingApprovalId: string, approvalRef: string): boolean {
|
|
171
|
+
const normalizedBindingId = normalizeApprovalRef(bindingApprovalId);
|
|
172
|
+
const normalizedRef = normalizeApprovalRef(approvalRef);
|
|
173
|
+
if (!normalizedBindingId || !normalizedRef) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
return (
|
|
177
|
+
normalizedRef === normalizedBindingId ||
|
|
178
|
+
(normalizedRef.length >= 8 && normalizedBindingId.startsWith(normalizedRef))
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function pruneExpiredGoogleChatApprovalCardBindings(nowMs: number): void {
|
|
183
|
+
for (const [token, binding] of approvalCardBindings) {
|
|
184
|
+
if (binding.expiresAtMs <= nowMs) {
|
|
185
|
+
approvalCardBindings.delete(token);
|
|
186
|
+
approvalCardResolvingTokens.delete(token);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const [approvalId, suppression] of manualApprovalFollowupSuppressions) {
|
|
190
|
+
if (suppression.expiresAtMs <= nowMs) {
|
|
191
|
+
manualApprovalFollowupSuppressions.delete(approvalId);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function hasActiveGoogleChatExecApprovalCardForManualCommand(params: {
|
|
197
|
+
approvalRef: string;
|
|
198
|
+
decision: ExecApprovalDecision;
|
|
199
|
+
nowMs: number;
|
|
200
|
+
}): boolean {
|
|
201
|
+
pruneExpiredGoogleChatApprovalCardBindings(params.nowMs);
|
|
202
|
+
for (const binding of approvalCardBindings.values()) {
|
|
203
|
+
if (
|
|
204
|
+
binding.approvalKind === "exec" &&
|
|
205
|
+
binding.allowedDecisions.includes(params.decision) &&
|
|
206
|
+
approvalRefMatches(binding.approvalId, params.approvalRef)
|
|
207
|
+
) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
for (const suppression of manualApprovalFollowupSuppressions.values()) {
|
|
212
|
+
if (
|
|
213
|
+
suppression.approvalKind === "exec" &&
|
|
214
|
+
suppression.allowedDecisions.includes(params.decision) &&
|
|
215
|
+
approvalRefMatches(suppression.approvalId, params.approvalRef)
|
|
216
|
+
) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function shouldSuppressGoogleChatManualExecApprovalFollowupText(
|
|
224
|
+
text: string,
|
|
225
|
+
nowMs = Date.now(),
|
|
226
|
+
): boolean {
|
|
227
|
+
for (const match of text.matchAll(MANUAL_EXEC_APPROVAL_COMMAND_RE)) {
|
|
228
|
+
const approvalRef = match[1];
|
|
229
|
+
const decision = match[2]?.toLowerCase() as ExecApprovalDecision | undefined;
|
|
230
|
+
if (
|
|
231
|
+
approvalRef &&
|
|
232
|
+
decision &&
|
|
233
|
+
hasActiveGoogleChatExecApprovalCardForManualCommand({ approvalRef, decision, nowMs })
|
|
234
|
+
) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function hasSendableMedia(payload: GoogleChatManualApprovalSuppressionPayload): boolean {
|
|
242
|
+
return Boolean(payload.mediaUrl?.trim() || payload.mediaUrls?.some((url) => url.trim()));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function hasStructuredPayloadPart(payload: GoogleChatManualApprovalSuppressionPayload): boolean {
|
|
246
|
+
return Boolean(
|
|
247
|
+
hasSendableMedia(payload) ||
|
|
248
|
+
payload.presentation ||
|
|
249
|
+
payload.interactive ||
|
|
250
|
+
payload.btw ||
|
|
251
|
+
payload.spokenText ||
|
|
252
|
+
payload.ttsSupplement,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function shouldSuppressGoogleChatManualExecApprovalFollowupPayload(
|
|
257
|
+
payload: GoogleChatManualApprovalSuppressionPayload,
|
|
258
|
+
nowMs = Date.now(),
|
|
259
|
+
): boolean {
|
|
260
|
+
const text = payload.text?.trim();
|
|
261
|
+
if (!text || hasStructuredPayloadPart(payload)) {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
return shouldSuppressGoogleChatManualExecApprovalFollowupText(text, nowMs);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function claimGoogleChatApprovalCardBinding(token: string): GoogleChatApprovalCardClaim {
|
|
268
|
+
const binding = getGoogleChatApprovalCardBinding(token);
|
|
269
|
+
if (!binding) {
|
|
270
|
+
return { kind: "missing" };
|
|
271
|
+
}
|
|
272
|
+
if (approvalCardResolvingTokens.has(token)) {
|
|
273
|
+
return { kind: "in-flight" };
|
|
274
|
+
}
|
|
275
|
+
approvalCardResolvingTokens.add(token);
|
|
276
|
+
return { kind: "claimed", binding };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function completeGoogleChatApprovalCardBinding(token: string): void {
|
|
280
|
+
const binding = approvalCardBindings.get(token);
|
|
281
|
+
approvalCardResolvingTokens.delete(token);
|
|
282
|
+
approvalCardBindings.delete(token);
|
|
283
|
+
if (binding) {
|
|
284
|
+
unregisterGoogleChatManualApprovalFollowupSuppression(binding.approvalId);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function releaseGoogleChatApprovalCardBinding(token: string): void {
|
|
289
|
+
approvalCardResolvingTokens.delete(token);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function unregisterGoogleChatApprovalCardBindings(tokens: readonly string[]): void {
|
|
293
|
+
for (const token of tokens) {
|
|
294
|
+
const binding = approvalCardBindings.get(token);
|
|
295
|
+
approvalCardBindings.delete(token);
|
|
296
|
+
approvalCardResolvingTokens.delete(token);
|
|
297
|
+
if (binding) {
|
|
298
|
+
unregisterGoogleChatManualApprovalFollowupSuppression(binding.approvalId);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function clearGoogleChatApprovalCardBindingsForTest(): void {
|
|
304
|
+
approvalCardBindings.clear();
|
|
305
|
+
approvalCardResolvingTokens.clear();
|
|
306
|
+
manualApprovalFollowupSuppressions.clear();
|
|
307
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildGoogleChatApprovalActionParameters,
|
|
4
|
+
clearGoogleChatApprovalCardBindingsForTest,
|
|
5
|
+
registerGoogleChatApprovalCardBinding,
|
|
6
|
+
} from "./approval-card-actions.js";
|
|
7
|
+
import { maybeHandleGoogleChatApprovalCardClick } from "./approval-card-click.js";
|
|
8
|
+
import type { WebhookTarget } from "./monitor-types.js";
|
|
9
|
+
import type { GoogleChatEvent } from "./types.js";
|
|
10
|
+
|
|
11
|
+
const resolveApprovalOverGateway = vi.hoisted(() => vi.fn());
|
|
12
|
+
|
|
13
|
+
vi.mock("actagent/plugin-sdk/approval-gateway-runtime", () => ({
|
|
14
|
+
resolveApprovalOverGateway,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
function createTarget(): WebhookTarget {
|
|
18
|
+
return {
|
|
19
|
+
account: {
|
|
20
|
+
accountId: "default",
|
|
21
|
+
enabled: true,
|
|
22
|
+
credentialSource: "inline",
|
|
23
|
+
config: {
|
|
24
|
+
dm: { allowFrom: ["users/123"] },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
config: {
|
|
28
|
+
channels: {
|
|
29
|
+
googlechat: {
|
|
30
|
+
dm: { allowFrom: ["users/123"] },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
runtime: { log: vi.fn(), error: vi.fn() },
|
|
35
|
+
core: {} as never,
|
|
36
|
+
path: "/googlechat",
|
|
37
|
+
mediaMaxMb: 20,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function createCardClickEvent(token: string, userName = "users/123"): GoogleChatEvent {
|
|
42
|
+
return {
|
|
43
|
+
type: "CARD_CLICKED",
|
|
44
|
+
space: { name: "spaces/AAA" },
|
|
45
|
+
message: { name: "spaces/AAA/messages/msg-1" },
|
|
46
|
+
user: { name: userName },
|
|
47
|
+
action: {
|
|
48
|
+
actionMethodName: "actagent.approval",
|
|
49
|
+
parameters: buildGoogleChatApprovalActionParameters(token),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("maybeHandleGoogleChatApprovalCardClick", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
clearGoogleChatApprovalCardBindingsForTest();
|
|
57
|
+
resolveApprovalOverGateway.mockReset();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("authorizes the Chat actor and resolves the bound approval over the gateway", async () => {
|
|
61
|
+
registerGoogleChatApprovalCardBinding({
|
|
62
|
+
token: "token-1",
|
|
63
|
+
accountId: "default",
|
|
64
|
+
approvalId: "approval-1",
|
|
65
|
+
approvalKind: "exec",
|
|
66
|
+
decision: "allow-once",
|
|
67
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
68
|
+
spaceName: "spaces/AAA",
|
|
69
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
70
|
+
expiresAtMs: Date.now() + 60_000,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await expect(
|
|
74
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
75
|
+
event: createCardClickEvent("token-1"),
|
|
76
|
+
target: createTarget(),
|
|
77
|
+
}),
|
|
78
|
+
).resolves.toBe(true);
|
|
79
|
+
|
|
80
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledWith({
|
|
81
|
+
cfg: expect.any(Object),
|
|
82
|
+
approvalId: "approval-1",
|
|
83
|
+
decision: "allow-once",
|
|
84
|
+
senderId: "users/123",
|
|
85
|
+
allowPluginFallback: true,
|
|
86
|
+
clientDisplayName: "Google Chat approval (users/123)",
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("accepts add-on clicks that only carry approval token parameters", async () => {
|
|
91
|
+
registerGoogleChatApprovalCardBinding({
|
|
92
|
+
token: "token-addon",
|
|
93
|
+
accountId: "default",
|
|
94
|
+
approvalId: "approval-addon",
|
|
95
|
+
approvalKind: "exec",
|
|
96
|
+
decision: "allow-once",
|
|
97
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
98
|
+
spaceName: "spaces/AAA",
|
|
99
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
100
|
+
expiresAtMs: Date.now() + 60_000,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await expect(
|
|
104
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
105
|
+
event: {
|
|
106
|
+
type: "CARD_CLICKED",
|
|
107
|
+
space: { name: "spaces/AAA" },
|
|
108
|
+
message: { name: "spaces/AAA/messages/msg-1" },
|
|
109
|
+
user: { name: "users/123" },
|
|
110
|
+
commonEventObject: {
|
|
111
|
+
parameters: {
|
|
112
|
+
actagent_action: "approval",
|
|
113
|
+
token: "token-addon",
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
target: createTarget(),
|
|
118
|
+
}),
|
|
119
|
+
).resolves.toBe(true);
|
|
120
|
+
|
|
121
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledWith(
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
approvalId: "approval-addon",
|
|
124
|
+
decision: "allow-once",
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("accepts standard cardsV2 clicks with common parameters", async () => {
|
|
130
|
+
registerGoogleChatApprovalCardBinding({
|
|
131
|
+
token: "token-common",
|
|
132
|
+
accountId: "default",
|
|
133
|
+
approvalId: "approval-common",
|
|
134
|
+
approvalKind: "plugin",
|
|
135
|
+
decision: "deny",
|
|
136
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
137
|
+
spaceName: "spaces/AAA",
|
|
138
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
139
|
+
expiresAtMs: Date.now() + 60_000,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await expect(
|
|
143
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
144
|
+
event: {
|
|
145
|
+
type: "CARD_CLICKED",
|
|
146
|
+
space: { name: "spaces/AAA" },
|
|
147
|
+
message: { name: "spaces/AAA/messages/msg-1" },
|
|
148
|
+
user: { name: "users/123" },
|
|
149
|
+
common: {
|
|
150
|
+
invokedFunction: "actagent.approval",
|
|
151
|
+
parameters: {
|
|
152
|
+
actagent_action: "approval",
|
|
153
|
+
token: "token-common",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
target: createTarget(),
|
|
158
|
+
}),
|
|
159
|
+
).resolves.toBe(true);
|
|
160
|
+
|
|
161
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
approvalId: "approval-common",
|
|
164
|
+
decision: "deny",
|
|
165
|
+
allowPluginFallback: false,
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("accepts endpoint URL invoked functions for app-url card actions", async () => {
|
|
171
|
+
registerGoogleChatApprovalCardBinding({
|
|
172
|
+
token: "token-url",
|
|
173
|
+
accountId: "default",
|
|
174
|
+
approvalId: "approval-url",
|
|
175
|
+
approvalKind: "exec",
|
|
176
|
+
decision: "allow-once",
|
|
177
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
178
|
+
spaceName: "spaces/AAA",
|
|
179
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
180
|
+
expiresAtMs: Date.now() + 60_000,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
await expect(
|
|
184
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
185
|
+
event: {
|
|
186
|
+
type: "CARD_CLICKED",
|
|
187
|
+
space: { name: "spaces/AAA" },
|
|
188
|
+
message: { name: "spaces/AAA/messages/msg-1" },
|
|
189
|
+
user: { name: "users/123" },
|
|
190
|
+
commonEventObject: {
|
|
191
|
+
invokedFunction: "https://chat-app.example.test/googlechat",
|
|
192
|
+
parameters: {
|
|
193
|
+
actagent_action: "approval",
|
|
194
|
+
token: "token-url",
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
target: createTarget(),
|
|
199
|
+
}),
|
|
200
|
+
).resolves.toBe(true);
|
|
201
|
+
|
|
202
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledWith(
|
|
203
|
+
expect.objectContaining({
|
|
204
|
+
approvalId: "approval-url",
|
|
205
|
+
decision: "allow-once",
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not consume the token when an unauthorized user clicks", async () => {
|
|
211
|
+
registerGoogleChatApprovalCardBinding({
|
|
212
|
+
token: "token-2",
|
|
213
|
+
accountId: "default",
|
|
214
|
+
approvalId: "plugin:approval-2",
|
|
215
|
+
approvalKind: "plugin",
|
|
216
|
+
decision: "deny",
|
|
217
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
218
|
+
spaceName: "spaces/AAA",
|
|
219
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
220
|
+
expiresAtMs: Date.now() + 60_000,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await expect(
|
|
224
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
225
|
+
event: createCardClickEvent("token-2", "users/999"),
|
|
226
|
+
target: createTarget(),
|
|
227
|
+
}),
|
|
228
|
+
).resolves.toBe(true);
|
|
229
|
+
|
|
230
|
+
expect(resolveApprovalOverGateway).not.toHaveBeenCalled();
|
|
231
|
+
|
|
232
|
+
await expect(
|
|
233
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
234
|
+
event: createCardClickEvent("token-2", "users/123"),
|
|
235
|
+
target: createTarget(),
|
|
236
|
+
}),
|
|
237
|
+
).resolves.toBe(true);
|
|
238
|
+
|
|
239
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledWith(
|
|
240
|
+
expect.objectContaining({
|
|
241
|
+
approvalId: "plugin:approval-2",
|
|
242
|
+
decision: "deny",
|
|
243
|
+
allowPluginFallback: false,
|
|
244
|
+
}),
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("keeps the token retryable when gateway resolution fails", async () => {
|
|
249
|
+
registerGoogleChatApprovalCardBinding({
|
|
250
|
+
token: "token-retry",
|
|
251
|
+
accountId: "default",
|
|
252
|
+
approvalId: "approval-retry",
|
|
253
|
+
approvalKind: "exec",
|
|
254
|
+
decision: "allow-once",
|
|
255
|
+
allowedDecisions: ["allow-once", "deny"],
|
|
256
|
+
spaceName: "spaces/AAA",
|
|
257
|
+
messageName: "spaces/AAA/messages/msg-1",
|
|
258
|
+
expiresAtMs: Date.now() + 60_000,
|
|
259
|
+
});
|
|
260
|
+
resolveApprovalOverGateway.mockRejectedValueOnce(new Error("gateway unavailable"));
|
|
261
|
+
|
|
262
|
+
await expect(
|
|
263
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
264
|
+
event: createCardClickEvent("token-retry"),
|
|
265
|
+
target: createTarget(),
|
|
266
|
+
}),
|
|
267
|
+
).rejects.toThrow("gateway unavailable");
|
|
268
|
+
|
|
269
|
+
resolveApprovalOverGateway.mockResolvedValueOnce(undefined);
|
|
270
|
+
await expect(
|
|
271
|
+
maybeHandleGoogleChatApprovalCardClick({
|
|
272
|
+
event: createCardClickEvent("token-retry"),
|
|
273
|
+
target: createTarget(),
|
|
274
|
+
}),
|
|
275
|
+
).resolves.toBe(true);
|
|
276
|
+
|
|
277
|
+
expect(resolveApprovalOverGateway).toHaveBeenCalledTimes(2);
|
|
278
|
+
});
|
|
279
|
+
});
|