@mercury-fw/channel-google-chat 0.4.0 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # @mercury-fw/channel-google-chat
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c24de78: A Google Chat sender is recognised as their ZITADEL user.
8
+
9
+ - `channel-google-chat` passes the sender's email on as a claim (`claims.email`), on messages and on card clicks, when Google vouches for it: a person's (`HUMAN`) only.
10
+ - `zitadelDirectory` resolves a Google Chat sender to the one active ZITADEL user with that email, verified, whose links to the Google identity provider (`ZITADEL_GOOGLE_IDP_ID`, new and optional) include the sender's own Google account. Without that variable nobody is recognised from Chat. Anyone else on Chat stays unknown and can link their account with a code.
11
+ - `mfw create` lists `ZITADEL_GOOGLE_IDP_ID` in the env example of an app with the ZITADEL directory.
12
+
13
+ ## 0.5.0
14
+
15
+ ### Minor Changes
16
+
17
+ - a7f2f13: A user directory decides who people are and what they may make Mercury do.
18
+
19
+ - An app can declare a `directory` in `mercury.config.ts`. Given whoever a channel says is talking, it returns the person with their roles. The person's key is `<directory name>:<id>`, so one person reached from several channels keeps one private area, one memory and one set of confirmations.
20
+ - With a directory the instance is closed. Someone it doesn't know gets a refusal and nothing runs, unless `access.unknown` is `"allow"`; `access.unknownMessage` sets what they're told. A directory that can't be reached, or fails to load, refuses everyone but the terminal. Answers are cached for five minutes, so a revoked role stops counting within that time.
21
+ - `mercury.act-as-self` (every plugin) or `mercury.act-as-self.<plugin>` (one) lets a person make Mercury act with its own identity.
22
+ - A plugin acting as Mercury, such as atlassian-admin, is offered only to those people, and on the terminal.
23
+ - A plugin acting as the person also gives them each tool a second time, as `<tool>AsMercury`.
24
+ - Every call made as Mercury for someone is logged with who asked.
25
+ - `access.plugins` restricts a plugin to some roles. Prompts, skills and the manifest follow what each person is offered.
26
+ - New package `@mercury-fw/directory-static`: people, their identities on each channel and their roles, listed in `DIRECTORY_STATIC_PEOPLE`.
27
+ - The channel contract is at version 5:
28
+ - `ChannelRuntimeContext.admit` says whether the core talks to a caller;
29
+ - the reads are per person and async, the manifest included;
30
+ - `DirectoryPlugin` is the directory contract;
31
+ - `Principal` no longer carries `roles`.
32
+ - `channel-http` answers `403` to a caller the directory doesn't know and `503` when it can't be reached, on every route.
33
+ - `auth-static` refuses `roles` in `AUTH_STATIC_TOKENS`: roles come from the directory.
34
+
3
35
  ## 0.4.0
4
36
 
5
37
  ### Minor Changes
package/README.md CHANGED
@@ -24,6 +24,8 @@ channels: [googleChatChannel],
24
24
  | `GOOGLE_CHAT_APP_CLIENT_EMAIL` | The service account the app authenticates as. |
25
25
  | `GOOGLE_CHAT_APP_PRIVATE_KEY` | That service account's private key, on one line with literal `\n` (step 5 writes it that way). |
26
26
 
27
+ Each message reaches the core as `google-chat:users/<id>`, the sender's Chat user, with their email when Google vouches for it (a person's, never a bot's), which a user directory can join on: the ZITADEL directory of `@mercury-fw/plugin-zitadel` does.
28
+
27
29
  ## Table of contents
28
30
 
29
31
  - [Setting up the Chat app](#setting-up-the-chat-app)
@@ -41,6 +41,8 @@ export type ParsedMessageEvent = {
41
41
  space: string;
42
42
  sender: string;
43
43
  senderDisplayName: string | undefined;
44
+ /** The sender's email, which Google vouches for: a person's only, never a bot's. */
45
+ senderEmail: string | undefined;
44
46
  /**
45
47
  * True only when `space.type` is exactly `"DM"` (the Chat API's shape
46
48
  * for a private 1:1 space) — anything else, including a missing field,
@@ -54,11 +56,12 @@ export type ParsedCardClickEvent = {
54
56
  kind: "card-click";
55
57
  space: string;
56
58
  sender: string;
59
+ senderEmail: string | undefined;
57
60
  parameters: Record<string, string>;
58
61
  };
59
62
  /** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
60
63
  export declare function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null;
61
- export type CardClickHandler = (params: Record<string, string>, space: string, sender: string) => Promise<void>;
64
+ export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string) => Promise<void>;
62
65
  export type GoogleChatProviderDeps = {
63
66
  credentials: ServiceAccountCredentials;
64
67
  subscription: string;
@@ -197,6 +197,8 @@ export type ParsedMessageEvent = {
197
197
  space: string;
198
198
  sender: string;
199
199
  senderDisplayName: string | undefined;
200
+ /** The sender's email, which Google vouches for: a person's only, never a bot's. */
201
+ senderEmail: string | undefined;
200
202
  /**
201
203
  * True only when `space.type` is exactly `"DM"` (the Chat API's shape
202
204
  * for a private 1:1 space) — anything else, including a missing field,
@@ -211,9 +213,15 @@ export type ParsedCardClickEvent = {
211
213
  kind: "card-click";
212
214
  space: string;
213
215
  sender: string;
216
+ senderEmail: string | undefined;
214
217
  parameters: Record<string, string>;
215
218
  };
216
219
 
220
+ /** The email Google vouches for on `sender`: a person's only (`HUMAN`), never a bot's or an unknown kind's. */
221
+ function emailOf(sender: RawChatSender | undefined): string | undefined {
222
+ return typeof sender?.email === "string" && sender.email !== "" && sender.type === "HUMAN" ? sender.email : undefined;
223
+ }
224
+
217
225
  /** Parses one decoded Pub/Sub event, or `null` if it isn't a kind this provider acts on. */
218
226
  export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardClickEvent | null {
219
227
  const event = raw as RawChatEvent;
@@ -229,6 +237,7 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
229
237
  space: m.space.name,
230
238
  sender: m.sender.name,
231
239
  senderDisplayName: m.sender.displayName,
240
+ senderEmail: emailOf(m.sender),
232
241
  isDirectMessage: m.space.type === "DM",
233
242
  };
234
243
  }
@@ -241,12 +250,12 @@ export function parseChatEvent(raw: unknown): ParsedMessageEvent | ParsedCardCli
241
250
  parameters[p.key] = p.value;
242
251
  }
243
252
  }
244
- return { kind: "card-click", space: event.space.name, sender: event.user.name, parameters };
253
+ return { kind: "card-click", space: event.space.name, sender: event.user.name, senderEmail: emailOf(event.user), parameters };
245
254
  }
246
255
  return null;
247
256
  }
248
257
 
249
- export type CardClickHandler = (params: Record<string, string>, space: string, sender: string) => Promise<void>;
258
+ export type CardClickHandler = (params: Record<string, string>, space: string, sender: string, senderEmail?: string) => Promise<void>;
250
259
 
251
260
  export type GoogleChatProviderDeps = {
252
261
  credentials: ServiceAccountCredentials;
@@ -310,13 +319,17 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
310
319
  */
311
320
  const onCardClick: CardClickHandler =
312
321
  deps.onCardClick ??
313
- (async (params, space, sender) => {
322
+ (async (params, space, sender, senderEmail) => {
314
323
  const token = params.token;
315
324
  if (!token) {
316
325
  log(`[chat] card click with no token parameter`);
317
326
  return;
318
327
  }
319
- const reply = await deps.confirm(token, deriveSessionKey(space, sender), { id: sender, provider: "google-chat" });
328
+ const reply = await deps.confirm(token, deriveSessionKey(space, sender), {
329
+ id: sender,
330
+ provider: "google-chat",
331
+ ...(senderEmail === undefined ? {} : { claims: { email: senderEmail } }),
332
+ });
320
333
  if (reply !== null) {
321
334
  log(`[chat:${space}] [out] ${reply}`);
322
335
  const sent = await sendMessageFn(space, reply, clientDeps);
@@ -483,6 +496,8 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
483
496
  id: event.sender,
484
497
  provider: "google-chat",
485
498
  ...(event.senderDisplayName ? { displayName: event.senderDisplayName } : {}),
499
+ // A directory joins a Chat identity to a person on it (see the ZITADEL directory).
500
+ ...(event.senderEmail === undefined ? {} : { claims: { email: event.senderEmail } }),
486
501
  };
487
502
 
488
503
  const confirmReply = await deps.confirm(event.text, sessionKey, principal);
@@ -556,7 +571,7 @@ export function createGoogleChatProvider(deps: GoogleChatProviderDeps): GoogleCh
556
571
  if (parsed.kind === "message") {
557
572
  await processMessageEvent(parsed, handleTurn);
558
573
  } else {
559
- await onCardClick(parsed.parameters, parsed.space, parsed.sender);
574
+ await onCardClick(parsed.parameters, parsed.space, parsed.sender, parsed.senderEmail);
560
575
  }
561
576
  } catch (err) {
562
577
  log(`[chat] event handling failed: ${String(err)}`);
package/index.ts CHANGED
@@ -22,7 +22,7 @@ function require(env: ChannelRuntimeContext["env"], name: string): string {
22
22
  export const googleChatChannel: ChannelPlugin = {
23
23
  // The contract this channel is written for, as a literal: importing
24
24
  // CHANNEL_API_VERSION would report whichever contract is installed.
25
- apiVersion: 4,
25
+ apiVersion: 5,
26
26
  name: "google-chat",
27
27
  build: (ctx) => {
28
28
  // No subscription configured ⇒ this instance simply doesn't run Google Chat.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-google-chat",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -34,7 +34,7 @@
34
34
  "@mercury-fw/channel-types": ">=0.36.0 <1.0.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@mercury-fw/channel-types": "0.40.0",
37
+ "@mercury-fw/channel-types": "0.44.1",
38
38
  "@mercury-fw/typescript-config": "*",
39
39
  "@types/bun": "^1.4.2",
40
40
  "typescript": "^6.0.3"