@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 +32 -0
- package/README.md +2 -0
- package/dist/google-chat-provider.d.ts +4 -1
- package/google-chat-provider.ts +20 -5
- package/index.ts +1 -1
- package/package.json +2 -2
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;
|
package/google-chat-provider.ts
CHANGED
|
@@ -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), {
|
|
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:
|
|
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.
|
|
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.
|
|
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"
|