@mercury-fw/channel-google-chat 0.5.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,15 @@
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
+
3
13
  ## 0.5.0
4
14
 
5
15
  ### 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/channel-google-chat",
3
- "version": "0.5.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.42.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"