@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.
Files changed (73) hide show
  1. package/README.md +11 -0
  2. package/actagent.plugin.json +17 -0
  3. package/api.ts +4 -0
  4. package/channel-config-api.ts +2 -0
  5. package/channel-plugin-api.ts +2 -0
  6. package/config-api.ts +3 -0
  7. package/contract-api.ts +6 -0
  8. package/directory-contract-api.ts +7 -0
  9. package/doctor-contract-api.ts +2 -0
  10. package/index.ts +21 -0
  11. package/npm-shrinkwrap.json +314 -0
  12. package/package.json +88 -0
  13. package/runtime-api.ts +61 -0
  14. package/secret-contract-api.ts +6 -0
  15. package/setup-entry.ts +14 -0
  16. package/setup-plugin-api.ts +3 -0
  17. package/src/accounts.ts +185 -0
  18. package/src/actions.test.ts +312 -0
  19. package/src/actions.ts +228 -0
  20. package/src/api.ts +346 -0
  21. package/src/approval-auth.test.ts +25 -0
  22. package/src/approval-auth.ts +38 -0
  23. package/src/approval-card-actions.test.ts +113 -0
  24. package/src/approval-card-actions.ts +307 -0
  25. package/src/approval-card-click.test.ts +279 -0
  26. package/src/approval-card-click.ts +94 -0
  27. package/src/approval-handler.runtime.test.ts +388 -0
  28. package/src/approval-handler.runtime.ts +413 -0
  29. package/src/approval-native.test.ts +399 -0
  30. package/src/approval-native.ts +246 -0
  31. package/src/auth.ts +219 -0
  32. package/src/channel-base.ts +123 -0
  33. package/src/channel-config.test.ts +174 -0
  34. package/src/channel.adapters.ts +363 -0
  35. package/src/channel.deps.runtime.ts +30 -0
  36. package/src/channel.runtime.ts +18 -0
  37. package/src/channel.setup.ts +7 -0
  38. package/src/channel.test.ts +845 -0
  39. package/src/channel.ts +214 -0
  40. package/src/config-schema.test.ts +32 -0
  41. package/src/config-schema.ts +4 -0
  42. package/src/doctor-contract.test.ts +76 -0
  43. package/src/doctor-contract.ts +181 -0
  44. package/src/doctor.ts +58 -0
  45. package/src/gateway.ts +84 -0
  46. package/src/google-auth.runtime.test.ts +571 -0
  47. package/src/google-auth.runtime.ts +570 -0
  48. package/src/group-policy.ts +18 -0
  49. package/src/monitor-access.test.ts +492 -0
  50. package/src/monitor-access.ts +466 -0
  51. package/src/monitor-durable.test.ts +40 -0
  52. package/src/monitor-durable.ts +24 -0
  53. package/src/monitor-reply-delivery.ts +162 -0
  54. package/src/monitor-routing.ts +66 -0
  55. package/src/monitor-types.ts +34 -0
  56. package/src/monitor-webhook.test.ts +670 -0
  57. package/src/monitor-webhook.ts +361 -0
  58. package/src/monitor.reply-delivery.test.ts +145 -0
  59. package/src/monitor.test.ts +389 -0
  60. package/src/monitor.ts +530 -0
  61. package/src/monitor.webhook-routing.test.ts +258 -0
  62. package/src/runtime.ts +10 -0
  63. package/src/secret-contract.test.ts +61 -0
  64. package/src/secret-contract.ts +162 -0
  65. package/src/setup-core.ts +41 -0
  66. package/src/setup-surface.ts +244 -0
  67. package/src/setup.test.ts +620 -0
  68. package/src/targets.test.ts +562 -0
  69. package/src/targets.ts +67 -0
  70. package/src/types.config.ts +4 -0
  71. package/src/types.ts +139 -0
  72. package/test-api.ts +3 -0
  73. package/tsconfig.json +16 -0
@@ -0,0 +1,258 @@
1
+ // Googlechat tests cover monitor.webhook routing plugin behavior.
2
+ import { EventEmitter } from "node:events";
3
+ import type { IncomingMessage } from "node:http";
4
+ import {
5
+ createEmptyPluginRegistry,
6
+ setActivePluginRegistry,
7
+ } from "actagent/plugin-sdk/plugin-test-runtime";
8
+ import { createMockServerResponse } from "actagent/plugin-sdk/test-env";
9
+ import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
10
+ import type { ACTAgentConfig, PluginRuntime } from "../runtime-api.js";
11
+ import type { ResolvedGoogleChatAccount } from "./accounts.js";
12
+ import { verifyGoogleChatRequest } from "./auth.js";
13
+ import {
14
+ handleGoogleChatWebhookRequest,
15
+ registerGoogleChatWebhookTarget,
16
+ } from "./monitor-routing.js";
17
+
18
+ vi.mock("./auth.js", () => ({
19
+ verifyGoogleChatRequest: vi.fn(),
20
+ }));
21
+
22
+ function createWebhookRequest(params: {
23
+ authorization?: string;
24
+ payload: unknown;
25
+ path?: string;
26
+ }): IncomingMessage {
27
+ const req = new EventEmitter() as IncomingMessage & {
28
+ destroyed?: boolean;
29
+ destroy: (error?: Error) => IncomingMessage;
30
+ on: (event: string, listener: (...args: unknown[]) => void) => IncomingMessage;
31
+ };
32
+ req.method = "POST";
33
+ req.url = params.path ?? "/googlechat";
34
+ req.headers = {
35
+ authorization: params.authorization ?? "",
36
+ "content-type": "application/json",
37
+ };
38
+ req.destroyed = false;
39
+ (req as unknown as { socket: { remoteAddress: string } }).socket = {
40
+ remoteAddress: "127.0.0.1",
41
+ };
42
+ req.destroy = () => {
43
+ req.destroyed = true;
44
+ return req;
45
+ };
46
+
47
+ const originalOn = req.on.bind(req);
48
+ let bodyScheduled = false;
49
+ req.on = ((event: string, listener: (...args: unknown[]) => void) => {
50
+ const result = originalOn(event, listener);
51
+ if (!bodyScheduled && event === "data") {
52
+ bodyScheduled = true;
53
+ void Promise.resolve().then(() => {
54
+ req.emit("data", Buffer.from(JSON.stringify(params.payload), "utf-8"));
55
+ if (!req.destroyed) {
56
+ req.emit("end");
57
+ }
58
+ });
59
+ }
60
+ return result;
61
+ }) as IncomingMessage["on"];
62
+
63
+ return req;
64
+ }
65
+
66
+ function createHeaderOnlyWebhookRequest(params: {
67
+ authorization?: string;
68
+ path?: string;
69
+ }): IncomingMessage {
70
+ const req = new EventEmitter() as IncomingMessage;
71
+ req.method = "POST";
72
+ req.url = params.path ?? "/googlechat";
73
+ req.headers = {
74
+ authorization: params.authorization ?? "",
75
+ "content-type": "application/json",
76
+ };
77
+ (req as unknown as { socket: { remoteAddress: string } }).socket = {
78
+ remoteAddress: "127.0.0.1",
79
+ };
80
+ return req;
81
+ }
82
+
83
+ const baseAccount = (accountId: string) =>
84
+ ({
85
+ accountId,
86
+ enabled: true,
87
+ credentialSource: "none",
88
+ config: {},
89
+ }) as ResolvedGoogleChatAccount;
90
+
91
+ function registerTwoTargets() {
92
+ const sinkA = vi.fn();
93
+ const sinkB = vi.fn();
94
+ const logA = vi.fn();
95
+ const logB = vi.fn();
96
+ const core = {} as PluginRuntime;
97
+ const config = {} as ACTAgentConfig;
98
+
99
+ const unregisterA = registerGoogleChatWebhookTarget({
100
+ account: baseAccount("A"),
101
+ config,
102
+ runtime: { log: logA },
103
+ core,
104
+ path: "/googlechat",
105
+ statusSink: sinkA,
106
+ mediaMaxMb: 5,
107
+ });
108
+ const unregisterB = registerGoogleChatWebhookTarget({
109
+ account: baseAccount("B"),
110
+ config,
111
+ runtime: { log: logB },
112
+ core,
113
+ path: "/googlechat",
114
+ statusSink: sinkB,
115
+ mediaMaxMb: 5,
116
+ });
117
+
118
+ return {
119
+ logA,
120
+ logB,
121
+ sinkA,
122
+ sinkB,
123
+ unregister: () => {
124
+ unregisterA();
125
+ unregisterB();
126
+ },
127
+ };
128
+ }
129
+
130
+ async function dispatchWebhookRequest(req: IncomingMessage) {
131
+ const res = createMockServerResponse();
132
+ const handled = await handleGoogleChatWebhookRequest(req, res);
133
+ expect(handled).toBe(true);
134
+ return res;
135
+ }
136
+
137
+ async function expectVerifiedRoute(params: {
138
+ request: IncomingMessage;
139
+ expectedStatus: number;
140
+ sinkA: ReturnType<typeof vi.fn>;
141
+ sinkB: ReturnType<typeof vi.fn>;
142
+ expectedSink: "none" | "A" | "B";
143
+ }) {
144
+ const res = await dispatchWebhookRequest(params.request);
145
+ expect(res.statusCode).toBe(params.expectedStatus);
146
+ const expectedCounts =
147
+ params.expectedSink === "A" ? [1, 0] : params.expectedSink === "B" ? [0, 1] : [0, 0];
148
+ expect(params.sinkA).toHaveBeenCalledTimes(expectedCounts[0]);
149
+ expect(params.sinkB).toHaveBeenCalledTimes(expectedCounts[1]);
150
+ }
151
+
152
+ function mockSecondVerifierSuccess() {
153
+ vi.mocked(verifyGoogleChatRequest)
154
+ .mockResolvedValueOnce({ ok: false, reason: "invalid" })
155
+ .mockResolvedValueOnce({ ok: true });
156
+ }
157
+
158
+ describe("Google Chat webhook routing", () => {
159
+ afterEach(() => {
160
+ setActivePluginRegistry(createEmptyPluginRegistry());
161
+ });
162
+
163
+ afterAll(() => {
164
+ vi.doUnmock("./auth.js");
165
+ vi.resetModules();
166
+ });
167
+
168
+ it("rejects ambiguous routing when multiple targets on the same path verify successfully", async () => {
169
+ vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: true });
170
+
171
+ const { sinkA, sinkB, unregister } = registerTwoTargets();
172
+
173
+ try {
174
+ await expectVerifiedRoute({
175
+ request: createWebhookRequest({
176
+ authorization: "Bearer test-token",
177
+ payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/AAA" } },
178
+ }),
179
+ expectedStatus: 401,
180
+ sinkA,
181
+ sinkB,
182
+ expectedSink: "none",
183
+ });
184
+ } finally {
185
+ unregister();
186
+ }
187
+ });
188
+
189
+ it("routes to the single verified target when earlier targets fail verification", async () => {
190
+ mockSecondVerifierSuccess();
191
+
192
+ const { logA, logB, sinkA, sinkB, unregister } = registerTwoTargets();
193
+
194
+ try {
195
+ await expectVerifiedRoute({
196
+ request: createWebhookRequest({
197
+ authorization: "Bearer test-token",
198
+ payload: { type: "ADDED_TO_SPACE", space: { name: "spaces/BBB" } },
199
+ }),
200
+ expectedStatus: 200,
201
+ sinkA,
202
+ sinkB,
203
+ expectedSink: "B",
204
+ });
205
+ expect(logA).not.toHaveBeenCalled();
206
+ expect(logB).not.toHaveBeenCalled();
207
+ } finally {
208
+ unregister();
209
+ }
210
+ });
211
+
212
+ it("rejects invalid bearer before attempting to read the body", async () => {
213
+ vi.mocked(verifyGoogleChatRequest).mockResolvedValue({ ok: false, reason: "invalid" });
214
+ const { unregister } = registerTwoTargets();
215
+
216
+ try {
217
+ const req = createHeaderOnlyWebhookRequest({
218
+ authorization: "Bearer invalid-token",
219
+ });
220
+ const onSpy = vi.spyOn(req, "on");
221
+ const res = await dispatchWebhookRequest(req);
222
+ expect(res.statusCode).toBe(401);
223
+ expect(onSpy.mock.calls.map(([event]) => event)).not.toContain("data");
224
+ } finally {
225
+ unregister();
226
+ }
227
+ });
228
+
229
+ it("supports add-on requests that provide systemIdToken in the body", async () => {
230
+ mockSecondVerifierSuccess();
231
+ const { sinkA, sinkB, unregister } = registerTwoTargets();
232
+
233
+ try {
234
+ await expectVerifiedRoute({
235
+ request: createWebhookRequest({
236
+ payload: {
237
+ commonEventObject: { hostApp: "CHAT" },
238
+ authorizationEventObject: { systemIdToken: "addon-token" },
239
+ chat: {
240
+ eventTime: "2026-03-02T00:00:00.000Z",
241
+ user: { name: "users/12345", displayName: "Test User" },
242
+ messagePayload: {
243
+ space: { name: "spaces/AAA" },
244
+ message: { text: "Hello from add-on" },
245
+ },
246
+ },
247
+ },
248
+ }),
249
+ expectedStatus: 200,
250
+ sinkA,
251
+ sinkB,
252
+ expectedSink: "B",
253
+ });
254
+ } finally {
255
+ unregister();
256
+ }
257
+ });
258
+ });
package/src/runtime.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Googlechat plugin module implements runtime behavior.
2
+ import { createPluginRuntimeStore } from "actagent/plugin-sdk/runtime-store";
3
+ import type { PluginRuntime } from "actagent/plugin-sdk/runtime-store";
4
+
5
+ const { setRuntime: setGoogleChatRuntime, getRuntime: getGoogleChatRuntime } =
6
+ createPluginRuntimeStore<PluginRuntime>({
7
+ pluginId: "googlechat",
8
+ errorMessage: "Google Chat runtime not initialized",
9
+ });
10
+ export { getGoogleChatRuntime, setGoogleChatRuntime };
@@ -0,0 +1,61 @@
1
+ // Googlechat tests cover secret contract plugin behavior.
2
+ import type { ACTAgentConfig } from "actagent/plugin-sdk/config-contracts";
3
+ import {
4
+ applyResolvedAssignments,
5
+ createResolverContext,
6
+ resolveSecretRefValues,
7
+ } from "actagent/plugin-sdk/secret-ref-runtime";
8
+ import { describe, expect, it } from "vitest";
9
+ import { collectRuntimeConfigAssignments } from "./secret-contract.js";
10
+
11
+ describe("googlechat secret contract", () => {
12
+ it("resolves account serviceAccount SecretRefs for enabled accounts", async () => {
13
+ const sourceConfig = {
14
+ channels: {
15
+ googlechat: {
16
+ enabled: true,
17
+ accounts: {
18
+ work: {
19
+ enabled: true,
20
+ serviceAccountRef: {
21
+ source: "env",
22
+ provider: "default",
23
+ id: "GOOGLECHAT_SERVICE_ACCOUNT",
24
+ },
25
+ },
26
+ },
27
+ },
28
+ },
29
+ } satisfies ACTAgentConfig;
30
+ const resolvedConfig: ACTAgentConfig = structuredClone(sourceConfig);
31
+ const context = createResolverContext({
32
+ sourceConfig,
33
+ env: {
34
+ GOOGLECHAT_SERVICE_ACCOUNT: '{"client_email":"bot@example.com"}',
35
+ },
36
+ });
37
+
38
+ collectRuntimeConfigAssignments({
39
+ config: resolvedConfig,
40
+ defaults: undefined,
41
+ context,
42
+ });
43
+
44
+ const resolved = await resolveSecretRefValues(
45
+ context.assignments.map((assignment) => assignment.ref),
46
+ {
47
+ config: sourceConfig,
48
+ env: context.env,
49
+ cache: context.cache,
50
+ },
51
+ );
52
+ applyResolvedAssignments({
53
+ assignments: context.assignments,
54
+ resolved,
55
+ });
56
+
57
+ const workAccount = resolvedConfig.channels?.googlechat?.accounts?.work;
58
+ expect(workAccount?.serviceAccount).toBe('{"client_email":"bot@example.com"}');
59
+ expect(context.warnings).toStrictEqual([]);
60
+ });
61
+ });
@@ -0,0 +1,162 @@
1
+ // Googlechat plugin module implements secret contract behavior.
2
+ import {
3
+ getChannelSurface,
4
+ hasOwnProperty,
5
+ pushAssignment,
6
+ pushInactiveSurfaceWarning,
7
+ pushWarning,
8
+ resolveChannelAccountSurface,
9
+ type ResolverContext,
10
+ type SecretDefaults,
11
+ } from "actagent/plugin-sdk/channel-secret-basic-runtime";
12
+ import { coerceSecretRef } from "actagent/plugin-sdk/secret-ref-runtime";
13
+
14
+ type GoogleChatAccountLike = {
15
+ serviceAccount?: unknown;
16
+ serviceAccountRef?: unknown;
17
+ accounts?: Record<string, unknown>;
18
+ };
19
+
20
+ export const secretTargetRegistryEntries: import("actagent/plugin-sdk/channel-secret-basic-runtime").SecretTargetRegistryEntry[] =
21
+ [
22
+ {
23
+ id: "channels.googlechat.accounts.*.serviceAccount",
24
+ targetType: "channels.googlechat.serviceAccount",
25
+ targetTypeAliases: ["channels.googlechat.accounts.*.serviceAccount"],
26
+ configFile: "actagent.json",
27
+ pathPattern: "channels.googlechat.accounts.*.serviceAccount",
28
+ refPathPattern: "channels.googlechat.accounts.*.serviceAccountRef",
29
+ secretShape: "sibling_ref",
30
+ expectedResolvedValue: "string-or-object",
31
+ includeInPlan: true,
32
+ includeInConfigure: true,
33
+ includeInAudit: true,
34
+ accountIdPathSegmentIndex: 3,
35
+ },
36
+ {
37
+ id: "channels.googlechat.serviceAccount",
38
+ targetType: "channels.googlechat.serviceAccount",
39
+ configFile: "actagent.json",
40
+ pathPattern: "channels.googlechat.serviceAccount",
41
+ refPathPattern: "channels.googlechat.serviceAccountRef",
42
+ secretShape: "sibling_ref",
43
+ expectedResolvedValue: "string-or-object",
44
+ includeInPlan: true,
45
+ includeInConfigure: true,
46
+ includeInAudit: true,
47
+ },
48
+ ];
49
+
50
+ function resolveSecretInputRef(params: {
51
+ value: unknown;
52
+ refValue?: unknown;
53
+ defaults?: SecretDefaults;
54
+ }) {
55
+ const explicitRef = coerceSecretRef(params.refValue, params.defaults);
56
+ const inlineRef = explicitRef ? null : coerceSecretRef(params.value, params.defaults);
57
+ return {
58
+ explicitRef,
59
+ inlineRef,
60
+ ref: explicitRef ?? inlineRef,
61
+ };
62
+ }
63
+
64
+ function collectGoogleChatAccountAssignment(params: {
65
+ target: GoogleChatAccountLike;
66
+ path: string;
67
+ defaults?: SecretDefaults;
68
+ context: ResolverContext;
69
+ active?: boolean;
70
+ inactiveReason?: string;
71
+ }): void {
72
+ const { explicitRef, ref } = resolveSecretInputRef({
73
+ value: params.target.serviceAccount,
74
+ refValue: params.target.serviceAccountRef,
75
+ defaults: params.defaults,
76
+ });
77
+ if (!ref) {
78
+ return;
79
+ }
80
+ if (params.active === false) {
81
+ pushInactiveSurfaceWarning({
82
+ context: params.context,
83
+ path: `${params.path}.serviceAccount`,
84
+ details: params.inactiveReason,
85
+ });
86
+ return;
87
+ }
88
+ if (
89
+ explicitRef &&
90
+ params.target.serviceAccount !== undefined &&
91
+ !coerceSecretRef(params.target.serviceAccount, params.defaults)
92
+ ) {
93
+ pushWarning(params.context, {
94
+ code: "SECRETS_REF_OVERRIDES_PLAINTEXT",
95
+ path: params.path,
96
+ message: `${params.path}: serviceAccountRef is set; runtime will ignore plaintext serviceAccount.`,
97
+ });
98
+ }
99
+ pushAssignment(params.context, {
100
+ ref,
101
+ path: `${params.path}.serviceAccount`,
102
+ expected: "string-or-object",
103
+ apply: (value) => {
104
+ params.target.serviceAccount = value;
105
+ },
106
+ });
107
+ }
108
+
109
+ export function collectRuntimeConfigAssignments(params: {
110
+ config: { channels?: Record<string, unknown> };
111
+ defaults?: SecretDefaults;
112
+ context: ResolverContext;
113
+ }): void {
114
+ const resolved = getChannelSurface(params.config, "googlechat");
115
+ if (!resolved) {
116
+ return;
117
+ }
118
+ const googleChat = resolved.channel as GoogleChatAccountLike;
119
+ const surface = resolveChannelAccountSurface(googleChat as Record<string, unknown>);
120
+ const topLevelServiceAccountActive = !surface.channelEnabled
121
+ ? false
122
+ : !surface.hasExplicitAccounts
123
+ ? true
124
+ : surface.accounts.some(
125
+ ({ account, enabled }) =>
126
+ enabled &&
127
+ !hasOwnProperty(account, "serviceAccount") &&
128
+ !hasOwnProperty(account, "serviceAccountRef"),
129
+ );
130
+ collectGoogleChatAccountAssignment({
131
+ target: googleChat,
132
+ path: "channels.googlechat",
133
+ defaults: params.defaults,
134
+ context: params.context,
135
+ active: topLevelServiceAccountActive,
136
+ inactiveReason: "no enabled account inherits this top-level Google Chat serviceAccount.",
137
+ });
138
+ if (!surface.hasExplicitAccounts) {
139
+ return;
140
+ }
141
+ for (const { accountId, account, enabled } of surface.accounts) {
142
+ if (
143
+ !hasOwnProperty(account, "serviceAccount") &&
144
+ !hasOwnProperty(account, "serviceAccountRef")
145
+ ) {
146
+ continue;
147
+ }
148
+ collectGoogleChatAccountAssignment({
149
+ target: account as GoogleChatAccountLike,
150
+ path: `channels.googlechat.accounts.${accountId}`,
151
+ defaults: params.defaults,
152
+ context: params.context,
153
+ active: enabled,
154
+ inactiveReason: "Google Chat account is disabled.",
155
+ });
156
+ }
157
+ }
158
+
159
+ export const channelSecrets = {
160
+ secretTargetRegistryEntries,
161
+ collectRuntimeConfigAssignments,
162
+ };
@@ -0,0 +1,41 @@
1
+ // Googlechat plugin module implements setup core behavior.
2
+ import {
3
+ createPatchedAccountSetupAdapter,
4
+ createSetupInputPresenceValidator,
5
+ } from "actagent/plugin-sdk/setup-runtime";
6
+
7
+ const channel = "googlechat" as const;
8
+
9
+ export const googlechatSetupAdapter = createPatchedAccountSetupAdapter({
10
+ channelKey: channel,
11
+ validateInput: createSetupInputPresenceValidator({
12
+ defaultAccountOnlyEnvError:
13
+ "GOOGLE_CHAT_SERVICE_ACCOUNT env vars can only be used for the default account.",
14
+ whenNotUseEnv: [
15
+ {
16
+ someOf: ["token", "tokenFile"],
17
+ message: "Google Chat requires --token (service account JSON) or --token-file.",
18
+ },
19
+ ],
20
+ }),
21
+ buildPatch: (input) => {
22
+ const patch = input.useEnv
23
+ ? {}
24
+ : input.tokenFile
25
+ ? { serviceAccountFile: input.tokenFile }
26
+ : input.token
27
+ ? { serviceAccount: input.token }
28
+ : {};
29
+ const audienceType = input.audienceType?.trim();
30
+ const audience = input.audience?.trim();
31
+ const webhookPath = input.webhookPath?.trim();
32
+ const webhookUrl = input.webhookUrl?.trim();
33
+ return {
34
+ ...patch,
35
+ ...(audienceType ? { audienceType } : {}),
36
+ ...(audience ? { audience } : {}),
37
+ ...(webhookPath ? { webhookPath } : {}),
38
+ ...(webhookUrl ? { webhookUrl } : {}),
39
+ };
40
+ },
41
+ });