crewly 1.2.6 → 1.2.8
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/dist/backend/backend/src/constants.d.ts +33 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +33 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts +7 -0
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js +67 -3
- package/dist/backend/backend/src/controllers/messaging/messenger.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.js +97 -2
- package/dist/backend/backend/src/controllers/settings/settings.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +149 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +2 -0
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +111 -82
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js +14 -3
- package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts +16 -2
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js +52 -5
- package/dist/backend/backend/src/services/agent/crewly-agent/model-manager.js.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js +22 -9
- package/dist/backend/backend/src/services/agent/gemini-runtime.service.js.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js +4 -3
- package/dist/backend/backend/src/services/event-bus/event-bus.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts +152 -23
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js +438 -63
- package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js +1 -1
- package/dist/backend/backend/src/services/messaging/message-queue.service.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/response-router.service.d.ts +20 -0
- package/dist/backend/backend/src/services/messaging/response-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/response-router.service.js +40 -13
- package/dist/backend/backend/src/services/messaging/response-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js +12 -3
- package/dist/backend/backend/src/services/orchestrator/orchestrator-restart.service.js.map +1 -1
- package/dist/backend/backend/src/services/settings/settings.service.d.ts +9 -1
- package/dist/backend/backend/src/services/settings/settings.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/settings/settings.service.js +12 -1
- package/dist/backend/backend/src/services/settings/settings.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js +13 -0
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js.map +1 -1
- package/dist/backend/backend/src/types/chat.types.d.ts +1 -1
- package/dist/backend/backend/src/types/chat.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/chat.types.js +1 -1
- package/dist/backend/backend/src/types/chat.types.js.map +1 -1
- package/dist/backend/backend/src/types/settings.types.d.ts +93 -0
- package/dist/backend/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/settings.types.js +131 -0
- package/dist/backend/backend/src/types/settings.types.js.map +1 -1
- package/dist/backend/backend/src/types/skill.types.js +2 -2
- package/dist/backend/backend/src/types/skill.types.js.map +1 -1
- package/dist/backend/backend/src/utils/format-error.d.ts +8 -0
- package/dist/backend/backend/src/utils/format-error.d.ts.map +1 -0
- package/dist/backend/backend/src/utils/format-error.js +10 -0
- package/dist/backend/backend/src/utils/format-error.js.map +1 -0
- package/dist/backend/backend/src/websocket/terminal.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/terminal.gateway.js +8 -6
- package/dist/backend/backend/src/websocket/terminal.gateway.js.map +1 -1
- package/dist/cli/backend/src/constants.d.ts +33 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +33 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/types/chat.types.d.ts +1 -1
- package/dist/cli/backend/src/types/chat.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/chat.types.js +1 -1
- package/dist/cli/backend/src/types/chat.types.js.map +1 -1
- package/dist/cli/backend/src/types/settings.types.d.ts +93 -0
- package/dist/cli/backend/src/types/settings.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/settings.types.js +131 -0
- package/dist/cli/backend/src/types/settings.types.js.map +1 -1
- package/dist/cli/backend/src/types/skill.types.js +2 -2
- package/dist/cli/backend/src/types/skill.types.js.map +1 -1
- package/frontend/dist/assets/index-58509b6a.js +4919 -0
- package/frontend/dist/assets/{index-4c4dcc31.css → index-ddb38eb0.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/index-3558d1a2.js +0 -4919
package/dist/backend/backend/src/services/messaging/adapters/google-chat-messenger.adapter.js
CHANGED
|
@@ -1,93 +1,153 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Google Chat Messenger Adapter
|
|
3
3
|
*
|
|
4
|
-
* Messenger adapter for Google Chat
|
|
5
|
-
*
|
|
6
|
-
* the Chat API
|
|
4
|
+
* Messenger adapter for Google Chat supporting three connection modes:
|
|
5
|
+
* 1. **Webhook mode** — post messages via an incoming webhook URL (send-only)
|
|
6
|
+
* 2. **Service account mode** — send messages via the Chat API (send-only)
|
|
7
|
+
* 3. **Pub/Sub mode** — pull incoming messages from a Cloud Pub/Sub subscription
|
|
8
|
+
* and reply via the Chat API (bidirectional, thread-aware)
|
|
9
|
+
*
|
|
10
|
+
* In Pub/Sub mode, a Google Chat App is configured to publish events to a
|
|
11
|
+
* Pub/Sub topic. Crewly pulls from the subscription, processes MESSAGE events,
|
|
12
|
+
* and replies in the same thread via the Chat API.
|
|
7
13
|
*
|
|
8
14
|
* @module services/messaging/adapters/google-chat-messenger.adapter
|
|
9
15
|
*/
|
|
10
|
-
|
|
11
|
-
const FETCH_TIMEOUT_MS = 15_000;
|
|
16
|
+
import { GOOGLE_CHAT_PUBSUB_CONSTANTS } from '../../../constants.js';
|
|
12
17
|
/**
|
|
13
|
-
* Messenger adapter for Google Chat.
|
|
14
|
-
*
|
|
15
|
-
* Supports two modes:
|
|
16
|
-
* 1. **Webhook mode** (simpler): uses an incoming webhook URL to post messages
|
|
17
|
-
* 2. **Service account mode**: uses a service account key to call the Chat API
|
|
18
|
+
* Messenger adapter for Google Chat with Pub/Sub support.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Supports webhook, service-account, and pubsub modes.
|
|
21
|
+
* Pub/Sub mode enables bidirectional communication with thread tracking.
|
|
21
22
|
*/
|
|
22
23
|
export class GoogleChatMessengerAdapter {
|
|
23
24
|
platform = 'google-chat';
|
|
25
|
+
/** Current connection mode */
|
|
26
|
+
mode = 'none';
|
|
24
27
|
/** Webhook URL for posting messages (webhook mode) */
|
|
25
28
|
webhookUrl = null;
|
|
26
|
-
/** Service account key JSON
|
|
29
|
+
/** Service account key JSON string */
|
|
27
30
|
serviceAccountKey = null;
|
|
28
|
-
/**
|
|
31
|
+
/** Authentication mode: service_account (JWT) or adc (refresh token) */
|
|
32
|
+
authMode = 'service_account';
|
|
33
|
+
/** ADC credentials (parsed from application_default_credentials.json) */
|
|
34
|
+
adcCredentials = null;
|
|
35
|
+
/** Access token obtained from service account or ADC (cached) */
|
|
29
36
|
accessToken = null;
|
|
30
37
|
/** Access token expiry timestamp */
|
|
31
38
|
tokenExpiresAt = 0;
|
|
39
|
+
/** OAuth2 scopes for the current mode */
|
|
40
|
+
tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
41
|
+
/** Pending token refresh promise to deduplicate concurrent requests (RL2) */
|
|
42
|
+
pendingTokenRefresh = null;
|
|
43
|
+
/** Full Pub/Sub subscription resource name (e.g. projects/PROJECT/subscriptions/SUB) */
|
|
44
|
+
subscriptionName = null;
|
|
45
|
+
/** GCP project ID (for Pub/Sub mode) */
|
|
46
|
+
projectId = null;
|
|
47
|
+
/** Pub/Sub pull interval timer */
|
|
48
|
+
pullIntervalTimer = null;
|
|
49
|
+
/** Callback for incoming messages */
|
|
50
|
+
onIncomingMessage = null;
|
|
51
|
+
/** Consecutive pull failure count */
|
|
52
|
+
consecutiveFailures = 0;
|
|
53
|
+
/** Whether the pull loop is paused due to failures */
|
|
54
|
+
pullPaused = false;
|
|
32
55
|
/**
|
|
33
|
-
* Initialize the adapter
|
|
56
|
+
* Initialize the adapter with the provided credentials.
|
|
57
|
+
*
|
|
58
|
+
* Detects mode based on provided config fields:
|
|
59
|
+
* - `webhookUrl` → webhook mode
|
|
60
|
+
* - `serviceAccountKey` + `projectId` + `subscriptionName` → pubsub mode
|
|
61
|
+
* - `serviceAccountKey` alone → service-account mode
|
|
34
62
|
*
|
|
35
|
-
* @param config -
|
|
36
|
-
* @throws Error if
|
|
63
|
+
* @param config - Configuration object with credentials
|
|
64
|
+
* @throws Error if credentials are invalid or missing
|
|
37
65
|
*/
|
|
38
66
|
async initialize(config) {
|
|
39
67
|
const webhookUrl = config.webhookUrl;
|
|
40
68
|
const serviceAccountKey = config.serviceAccountKey;
|
|
69
|
+
const projectId = config.projectId;
|
|
70
|
+
const subscriptionName = config.subscriptionName;
|
|
71
|
+
const onIncomingMessage = config.onIncomingMessage;
|
|
72
|
+
const authMode = (config.authMode === 'adc' ? 'adc' : 'service_account');
|
|
73
|
+
// Webhook mode
|
|
41
74
|
if (typeof webhookUrl === 'string' && webhookUrl) {
|
|
42
|
-
// Webhook mode: validate the URL format
|
|
43
75
|
if (!webhookUrl.startsWith('https://chat.googleapis.com/')) {
|
|
44
76
|
throw new Error('Invalid Google Chat webhook URL. Must start with https://chat.googleapis.com/');
|
|
45
77
|
}
|
|
46
|
-
|
|
47
|
-
// so we just validate the URL format and store it
|
|
78
|
+
this.resetState();
|
|
48
79
|
this.webhookUrl = webhookUrl;
|
|
49
|
-
this.
|
|
50
|
-
this.accessToken = null;
|
|
80
|
+
this.mode = 'webhook';
|
|
51
81
|
return;
|
|
52
82
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
// ADC mode — use Application Default Credentials (no SA key needed)
|
|
84
|
+
if (authMode === 'adc') {
|
|
85
|
+
const adcCreds = await this.loadAdcCredentials();
|
|
86
|
+
this.resetState();
|
|
87
|
+
this.authMode = 'adc';
|
|
88
|
+
this.adcCredentials = adcCreds;
|
|
89
|
+
// Pub/Sub mode: requires projectId + subscriptionName
|
|
90
|
+
if (typeof projectId === 'string' && projectId &&
|
|
91
|
+
typeof subscriptionName === 'string' && subscriptionName) {
|
|
92
|
+
this.projectId = projectId;
|
|
93
|
+
this.subscriptionName = `projects/${projectId}/subscriptions/${subscriptionName}`;
|
|
94
|
+
this.tokenScopes = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE} ${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_SCOPE}`;
|
|
95
|
+
this.mode = 'pubsub';
|
|
96
|
+
if (typeof onIncomingMessage === 'function') {
|
|
97
|
+
this.onIncomingMessage = onIncomingMessage;
|
|
59
98
|
}
|
|
99
|
+
this.startPubSubPull();
|
|
100
|
+
return;
|
|
60
101
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
102
|
+
// Service account mode with ADC auth (send-only)
|
|
103
|
+
this.mode = 'service-account';
|
|
104
|
+
this.tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// Service Account mode — requires a service account key
|
|
108
|
+
if (typeof serviceAccountKey === 'string' && serviceAccountKey) {
|
|
109
|
+
this.validateServiceAccountKey(serviceAccountKey);
|
|
110
|
+
this.resetState();
|
|
111
|
+
this.authMode = 'service_account';
|
|
112
|
+
this.serviceAccountKey = serviceAccountKey;
|
|
113
|
+
// Pub/Sub mode: requires projectId + subscriptionName
|
|
114
|
+
if (typeof projectId === 'string' && projectId &&
|
|
115
|
+
typeof subscriptionName === 'string' && subscriptionName) {
|
|
116
|
+
this.projectId = projectId;
|
|
117
|
+
this.subscriptionName = `projects/${projectId}/subscriptions/${subscriptionName}`;
|
|
118
|
+
this.tokenScopes = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE} ${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_SCOPE}`;
|
|
119
|
+
this.mode = 'pubsub';
|
|
120
|
+
if (typeof onIncomingMessage === 'function') {
|
|
121
|
+
this.onIncomingMessage = onIncomingMessage;
|
|
64
122
|
}
|
|
65
|
-
|
|
123
|
+
this.startPubSubPull();
|
|
124
|
+
return;
|
|
66
125
|
}
|
|
67
|
-
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
126
|
+
// Service account mode (send-only)
|
|
127
|
+
this.mode = 'service-account';
|
|
128
|
+
this.tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
70
129
|
return;
|
|
71
130
|
}
|
|
72
|
-
throw new Error('Google Chat requires either a webhookUrl or
|
|
131
|
+
throw new Error('Google Chat requires either a webhookUrl, serviceAccountKey, or authMode: adc');
|
|
73
132
|
}
|
|
74
133
|
/**
|
|
75
134
|
* Send a text message to a Google Chat space.
|
|
76
135
|
*
|
|
77
136
|
* In webhook mode, the `channel` parameter is ignored (webhook URL determines the space).
|
|
78
|
-
* In service
|
|
137
|
+
* In service-account/pubsub mode, `channel` is the space name (e.g., "spaces/AAAA...").
|
|
79
138
|
*
|
|
80
|
-
* @param channel - Google Chat space name (used in service
|
|
139
|
+
* @param channel - Google Chat space name (used in service-account/pubsub mode)
|
|
81
140
|
* @param text - Message content
|
|
82
141
|
* @param options - Optional send options (threadId for threaded replies)
|
|
83
142
|
* @throws Error if adapter is not initialized or send fails
|
|
84
143
|
*/
|
|
85
144
|
async sendMessage(channel, text, options) {
|
|
86
|
-
if (this.webhookUrl) {
|
|
145
|
+
if (this.mode === 'webhook' && this.webhookUrl) {
|
|
87
146
|
await this.sendViaWebhook(text, options?.threadId);
|
|
88
147
|
return;
|
|
89
148
|
}
|
|
90
|
-
if (this.
|
|
149
|
+
if ((this.mode === 'service-account' || this.mode === 'pubsub') &&
|
|
150
|
+
(this.serviceAccountKey || this.adcCredentials)) {
|
|
91
151
|
await this.sendViaApi(channel, text, options?.threadId);
|
|
92
152
|
return;
|
|
93
153
|
}
|
|
@@ -96,27 +156,174 @@ export class GoogleChatMessengerAdapter {
|
|
|
96
156
|
/**
|
|
97
157
|
* Get the current connection status.
|
|
98
158
|
*
|
|
99
|
-
* @returns Status object with connected flag and
|
|
159
|
+
* @returns Status object with connected flag, platform, and mode details
|
|
100
160
|
*/
|
|
101
161
|
getStatus() {
|
|
102
|
-
const connected = Boolean(this.webhookUrl || this.serviceAccountKey);
|
|
103
162
|
return {
|
|
104
|
-
connected,
|
|
163
|
+
connected: this.mode !== 'none',
|
|
105
164
|
platform: this.platform,
|
|
106
165
|
details: {
|
|
107
|
-
mode: this.
|
|
166
|
+
mode: this.mode,
|
|
167
|
+
authMode: this.authMode,
|
|
168
|
+
...(this.mode === 'pubsub' ? {
|
|
169
|
+
subscriptionName: this.subscriptionName,
|
|
170
|
+
projectId: this.projectId,
|
|
171
|
+
pullActive: Boolean(this.pullIntervalTimer) && !this.pullPaused,
|
|
172
|
+
pullPaused: this.pullPaused,
|
|
173
|
+
consecutiveFailures: this.consecutiveFailures,
|
|
174
|
+
} : {}),
|
|
108
175
|
},
|
|
109
176
|
};
|
|
110
177
|
}
|
|
111
178
|
/**
|
|
112
|
-
* Disconnect by clearing stored credentials.
|
|
179
|
+
* Disconnect by clearing stored credentials and stopping the pull loop.
|
|
113
180
|
*/
|
|
114
181
|
async disconnect() {
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
117
|
-
|
|
118
|
-
|
|
182
|
+
this.stopPubSubPull();
|
|
183
|
+
this.resetState();
|
|
184
|
+
}
|
|
185
|
+
// ===========================================================================
|
|
186
|
+
// Pub/Sub Pull Loop
|
|
187
|
+
// ===========================================================================
|
|
188
|
+
/**
|
|
189
|
+
* Start the Pub/Sub pull loop that periodically fetches messages.
|
|
190
|
+
*/
|
|
191
|
+
startPubSubPull() {
|
|
192
|
+
this.stopPubSubPull();
|
|
193
|
+
this.consecutiveFailures = 0;
|
|
194
|
+
this.pullPaused = false;
|
|
195
|
+
this.pullIntervalTimer = setInterval(async () => {
|
|
196
|
+
if (this.pullPaused)
|
|
197
|
+
return;
|
|
198
|
+
try {
|
|
199
|
+
await this.pullMessages();
|
|
200
|
+
this.consecutiveFailures = 0;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
this.consecutiveFailures++;
|
|
204
|
+
if (this.consecutiveFailures >= GOOGLE_CHAT_PUBSUB_CONSTANTS.MAX_CONSECUTIVE_FAILURES) {
|
|
205
|
+
this.pullPaused = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}, GOOGLE_CHAT_PUBSUB_CONSTANTS.PULL_INTERVAL_MS);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Stop the Pub/Sub pull loop.
|
|
212
|
+
*/
|
|
213
|
+
stopPubSubPull() {
|
|
214
|
+
if (this.pullIntervalTimer) {
|
|
215
|
+
clearInterval(this.pullIntervalTimer);
|
|
216
|
+
this.pullIntervalTimer = null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Pull messages from the Pub/Sub subscription, process them, and acknowledge.
|
|
221
|
+
*
|
|
222
|
+
* Each message is a base64-encoded Google Chat event JSON. Only MESSAGE-type
|
|
223
|
+
* events are forwarded to the incoming message callback. All messages are
|
|
224
|
+
* acknowledged regardless of type to prevent redelivery.
|
|
225
|
+
*/
|
|
226
|
+
async pullMessages() {
|
|
227
|
+
if (!this.subscriptionName) {
|
|
228
|
+
throw new Error('Pub/Sub subscription not configured');
|
|
229
|
+
}
|
|
230
|
+
const token = await this.getAccessToken();
|
|
231
|
+
const pullUrl = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_API_BASE}/${this.subscriptionName}:pull`;
|
|
232
|
+
const resp = await fetch(pullUrl, {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
headers: {
|
|
235
|
+
Authorization: `Bearer ${token}`,
|
|
236
|
+
'Content-Type': 'application/json',
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({ maxMessages: GOOGLE_CHAT_PUBSUB_CONSTANTS.MAX_MESSAGES_PER_PULL }),
|
|
239
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
240
|
+
});
|
|
241
|
+
if (!resp.ok) {
|
|
242
|
+
const details = await resp.text();
|
|
243
|
+
throw new Error(`Pub/Sub pull failed (${resp.status}): ${details}`);
|
|
244
|
+
}
|
|
245
|
+
const data = await resp.json();
|
|
246
|
+
if (!data.receivedMessages || data.receivedMessages.length === 0) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const ackIds = [];
|
|
250
|
+
for (const received of data.receivedMessages) {
|
|
251
|
+
ackIds.push(received.ackId);
|
|
252
|
+
if (!received.message.data)
|
|
253
|
+
continue;
|
|
254
|
+
try {
|
|
255
|
+
const decoded = Buffer.from(received.message.data, 'base64').toString('utf-8');
|
|
256
|
+
const event = JSON.parse(decoded);
|
|
257
|
+
this.processChatEvent(event);
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
// Skip malformed messages — they will still be acked
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
// Acknowledge all messages (even non-MESSAGE types) to prevent redelivery
|
|
264
|
+
if (ackIds.length > 0) {
|
|
265
|
+
await this.acknowledgeMessages(ackIds);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Process a Google Chat event and forward MESSAGE events to the callback.
|
|
270
|
+
*
|
|
271
|
+
* Thread tracking: extracts the thread name from the event so that replies
|
|
272
|
+
* can be posted back to the same thread.
|
|
273
|
+
*
|
|
274
|
+
* @param event - Parsed Google Chat event payload
|
|
275
|
+
*/
|
|
276
|
+
processChatEvent(event) {
|
|
277
|
+
// Only process MESSAGE events (ignore ADDED_TO_SPACE, REMOVED_FROM_SPACE, etc.)
|
|
278
|
+
if (event.type !== 'MESSAGE' || !event.message?.text) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!this.onIncomingMessage) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
const spaceName = event.space?.name || '';
|
|
285
|
+
const threadName = event.message.thread?.name || '';
|
|
286
|
+
const senderName = event.message.sender?.displayName || event.message.sender?.name || '';
|
|
287
|
+
const incomingMessage = {
|
|
288
|
+
platform: 'google-chat',
|
|
289
|
+
conversationId: spaceName,
|
|
290
|
+
channelId: spaceName,
|
|
291
|
+
userId: senderName,
|
|
292
|
+
text: event.message.text,
|
|
293
|
+
// Thread name is the full resource path (e.g. spaces/SPACE/threads/THREAD)
|
|
294
|
+
// This is used to reply in the same thread via the Chat API
|
|
295
|
+
threadId: threadName,
|
|
296
|
+
timestamp: event.message.createTime || event.eventTime || new Date().toISOString(),
|
|
297
|
+
};
|
|
298
|
+
this.onIncomingMessage(incomingMessage);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Acknowledge processed messages to prevent redelivery.
|
|
302
|
+
*
|
|
303
|
+
* @param ackIds - Array of ack IDs from the pull response
|
|
304
|
+
*/
|
|
305
|
+
async acknowledgeMessages(ackIds) {
|
|
306
|
+
if (!this.subscriptionName)
|
|
307
|
+
return;
|
|
308
|
+
const token = await this.getAccessToken();
|
|
309
|
+
const ackUrl = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.PUBSUB_API_BASE}/${this.subscriptionName}:acknowledge`;
|
|
310
|
+
const resp = await fetch(ackUrl, {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: {
|
|
313
|
+
Authorization: `Bearer ${token}`,
|
|
314
|
+
'Content-Type': 'application/json',
|
|
315
|
+
},
|
|
316
|
+
body: JSON.stringify({ ackIds }),
|
|
317
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
318
|
+
});
|
|
319
|
+
if (!resp.ok) {
|
|
320
|
+
const details = await resp.text();
|
|
321
|
+
throw new Error(`Pub/Sub acknowledge failed (${resp.status}): ${details}`);
|
|
322
|
+
}
|
|
119
323
|
}
|
|
324
|
+
// ===========================================================================
|
|
325
|
+
// Send Methods
|
|
326
|
+
// ===========================================================================
|
|
120
327
|
/**
|
|
121
328
|
* Send a message via incoming webhook URL.
|
|
122
329
|
*
|
|
@@ -141,7 +348,7 @@ export class GoogleChatMessengerAdapter {
|
|
|
141
348
|
method: 'POST',
|
|
142
349
|
headers: { 'Content-Type': 'application/json; charset=UTF-8' },
|
|
143
350
|
body: JSON.stringify(body),
|
|
144
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
351
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
145
352
|
});
|
|
146
353
|
if (!resp.ok) {
|
|
147
354
|
const details = await resp.text();
|
|
@@ -151,40 +358,53 @@ export class GoogleChatMessengerAdapter {
|
|
|
151
358
|
/**
|
|
152
359
|
* Send a message via the Google Chat REST API using service account credentials.
|
|
153
360
|
*
|
|
361
|
+
* When a threadId (thread name) is provided, the reply is posted to the same
|
|
362
|
+
* thread. This enables conversational thread tracking for Pub/Sub mode.
|
|
363
|
+
*
|
|
154
364
|
* @param space - Space name (e.g., "spaces/AAAA...")
|
|
155
365
|
* @param text - Message text
|
|
156
|
-
* @param
|
|
366
|
+
* @param threadName - Optional full thread name (e.g., "spaces/SPACE/threads/THREAD")
|
|
157
367
|
*/
|
|
158
|
-
async sendViaApi(space, text,
|
|
159
|
-
if (!this.serviceAccountKey) {
|
|
160
|
-
throw new Error('
|
|
368
|
+
async sendViaApi(space, text, threadName) {
|
|
369
|
+
if (!this.serviceAccountKey && !this.adcCredentials) {
|
|
370
|
+
throw new Error('No credentials configured (service account key or ADC)');
|
|
161
371
|
}
|
|
162
372
|
if (!space || !space.startsWith('spaces/')) {
|
|
163
373
|
throw new Error('Invalid Google Chat space name. Must start with "spaces/"');
|
|
164
374
|
}
|
|
165
375
|
const token = await this.getAccessToken();
|
|
166
376
|
const body = { text };
|
|
167
|
-
if (
|
|
168
|
-
|
|
377
|
+
if (threadName) {
|
|
378
|
+
// Use the full thread resource name for API-based threading
|
|
379
|
+
body.thread = { name: threadName };
|
|
169
380
|
}
|
|
170
|
-
|
|
381
|
+
// Build URL with messageReplyOption to enable thread replies
|
|
382
|
+
let url = `${GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_API_BASE}/${space}/messages`;
|
|
383
|
+
if (threadName) {
|
|
384
|
+
url += '?messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD';
|
|
385
|
+
}
|
|
386
|
+
const resp = await fetch(url, {
|
|
171
387
|
method: 'POST',
|
|
172
388
|
headers: {
|
|
173
389
|
Authorization: `Bearer ${token}`,
|
|
174
390
|
'Content-Type': 'application/json',
|
|
175
391
|
},
|
|
176
392
|
body: JSON.stringify(body),
|
|
177
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
393
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
178
394
|
});
|
|
179
395
|
if (!resp.ok) {
|
|
180
396
|
const details = await resp.text();
|
|
181
397
|
throw new Error(`Google Chat API send failed (${resp.status}): ${details}`);
|
|
182
398
|
}
|
|
183
399
|
}
|
|
400
|
+
// ===========================================================================
|
|
401
|
+
// Auth
|
|
402
|
+
// ===========================================================================
|
|
184
403
|
/**
|
|
185
404
|
* Get a valid access token, refreshing if needed.
|
|
186
405
|
*
|
|
187
|
-
* Uses
|
|
406
|
+
* Uses JWT-based OAuth2 for service accounts, or refresh_token flow for ADC.
|
|
407
|
+
* The scope includes Pub/Sub when in pubsub mode.
|
|
188
408
|
*
|
|
189
409
|
* @returns Valid access token string
|
|
190
410
|
*/
|
|
@@ -193,17 +413,48 @@ export class GoogleChatMessengerAdapter {
|
|
|
193
413
|
if (this.accessToken && Date.now() < this.tokenExpiresAt - 60_000) {
|
|
194
414
|
return this.accessToken;
|
|
195
415
|
}
|
|
416
|
+
// Deduplicate concurrent refresh requests (RL2)
|
|
417
|
+
if (this.pendingTokenRefresh) {
|
|
418
|
+
return this.pendingTokenRefresh;
|
|
419
|
+
}
|
|
420
|
+
this.pendingTokenRefresh = this.refreshAccessToken();
|
|
421
|
+
try {
|
|
422
|
+
return await this.pendingTokenRefresh;
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
this.pendingTokenRefresh = null;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Perform the actual token refresh.
|
|
430
|
+
*
|
|
431
|
+
* Dispatches to the appropriate flow based on authMode:
|
|
432
|
+
* - `service_account`: JWT-based OAuth2 flow with SA private key
|
|
433
|
+
* - `adc`: Refresh token flow using ADC credentials
|
|
434
|
+
*
|
|
435
|
+
* @returns Fresh access token string
|
|
436
|
+
*/
|
|
437
|
+
async refreshAccessToken() {
|
|
438
|
+
if (this.authMode === 'adc') {
|
|
439
|
+
return this.refreshAccessTokenViaAdc();
|
|
440
|
+
}
|
|
441
|
+
return this.refreshAccessTokenViaJwt();
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Refresh access token via JWT-based OAuth2 flow (service account).
|
|
445
|
+
*
|
|
446
|
+
* @returns Fresh access token string
|
|
447
|
+
*/
|
|
448
|
+
async refreshAccessTokenViaJwt() {
|
|
196
449
|
if (!this.serviceAccountKey) {
|
|
197
450
|
throw new Error('Service account key not configured');
|
|
198
451
|
}
|
|
199
|
-
// For service account auth, we need to create a JWT and exchange it for an access token.
|
|
200
|
-
// This is a simplified implementation — production use should use google-auth-library.
|
|
201
452
|
const key = JSON.parse(this.serviceAccountKey);
|
|
202
453
|
const now = Math.floor(Date.now() / 1000);
|
|
203
454
|
const header = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
204
455
|
const payload = Buffer.from(JSON.stringify({
|
|
205
456
|
iss: key.client_email,
|
|
206
|
-
scope:
|
|
457
|
+
scope: this.tokenScopes,
|
|
207
458
|
aud: 'https://oauth2.googleapis.com/token',
|
|
208
459
|
iat: now,
|
|
209
460
|
exp: now + 3600,
|
|
@@ -218,7 +469,7 @@ export class GoogleChatMessengerAdapter {
|
|
|
218
469
|
method: 'POST',
|
|
219
470
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
220
471
|
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`,
|
|
221
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
472
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
222
473
|
});
|
|
223
474
|
if (!resp.ok) {
|
|
224
475
|
const details = await resp.text();
|
|
@@ -229,5 +480,129 @@ export class GoogleChatMessengerAdapter {
|
|
|
229
480
|
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
230
481
|
return this.accessToken;
|
|
231
482
|
}
|
|
483
|
+
/**
|
|
484
|
+
* Refresh access token via ADC (Application Default Credentials).
|
|
485
|
+
*
|
|
486
|
+
* Uses the refresh_token from the ADC JSON file to obtain a new access token
|
|
487
|
+
* from Google's OAuth2 token endpoint. This supports credentials created by
|
|
488
|
+
* `gcloud auth application-default login`.
|
|
489
|
+
*
|
|
490
|
+
* @returns Fresh access token string
|
|
491
|
+
*/
|
|
492
|
+
async refreshAccessTokenViaAdc() {
|
|
493
|
+
if (!this.adcCredentials) {
|
|
494
|
+
throw new Error('ADC credentials not loaded');
|
|
495
|
+
}
|
|
496
|
+
const { client_id, client_secret, refresh_token } = this.adcCredentials;
|
|
497
|
+
const body = new URLSearchParams({
|
|
498
|
+
grant_type: 'refresh_token',
|
|
499
|
+
client_id,
|
|
500
|
+
client_secret,
|
|
501
|
+
refresh_token,
|
|
502
|
+
});
|
|
503
|
+
const resp = await fetch('https://oauth2.googleapis.com/token', {
|
|
504
|
+
method: 'POST',
|
|
505
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
506
|
+
body: body.toString(),
|
|
507
|
+
signal: AbortSignal.timeout(GOOGLE_CHAT_PUBSUB_CONSTANTS.FETCH_TIMEOUT_MS),
|
|
508
|
+
});
|
|
509
|
+
if (!resp.ok) {
|
|
510
|
+
const details = await resp.text();
|
|
511
|
+
throw new Error(`ADC token refresh failed (${resp.status}): ${details}`);
|
|
512
|
+
}
|
|
513
|
+
const data = await resp.json();
|
|
514
|
+
this.accessToken = data.access_token;
|
|
515
|
+
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
516
|
+
return this.accessToken;
|
|
517
|
+
}
|
|
518
|
+
// ===========================================================================
|
|
519
|
+
// Helpers
|
|
520
|
+
// ===========================================================================
|
|
521
|
+
/**
|
|
522
|
+
* Validate a service account key JSON string.
|
|
523
|
+
*
|
|
524
|
+
* @param key - JSON string of the service account key
|
|
525
|
+
* @throws Error if the key is invalid
|
|
526
|
+
*/
|
|
527
|
+
validateServiceAccountKey(key) {
|
|
528
|
+
try {
|
|
529
|
+
const parsed = JSON.parse(key);
|
|
530
|
+
if (!parsed.client_email || !parsed.private_key) {
|
|
531
|
+
throw new Error('Service account key must contain client_email and private_key');
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (err) {
|
|
535
|
+
if (err instanceof SyntaxError) {
|
|
536
|
+
throw new Error('Service account key must be valid JSON');
|
|
537
|
+
}
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Load ADC (Application Default Credentials) from the filesystem.
|
|
543
|
+
*
|
|
544
|
+
* Checks (in order):
|
|
545
|
+
* 1. `GOOGLE_APPLICATION_CREDENTIALS` environment variable (custom path)
|
|
546
|
+
* 2. Default gcloud ADC path: `~/.config/gcloud/application_default_credentials.json`
|
|
547
|
+
*
|
|
548
|
+
* @returns Parsed ADC credentials
|
|
549
|
+
* @throws Error if the ADC file is not found or invalid
|
|
550
|
+
*/
|
|
551
|
+
async loadAdcCredentials() {
|
|
552
|
+
const { readFile } = await import('node:fs/promises');
|
|
553
|
+
const { join } = await import('node:path');
|
|
554
|
+
const { homedir } = await import('node:os');
|
|
555
|
+
const envPath = process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
|
556
|
+
const defaultPath = join(homedir(), '.config', 'gcloud', 'application_default_credentials.json');
|
|
557
|
+
const credPath = envPath || defaultPath;
|
|
558
|
+
let raw;
|
|
559
|
+
try {
|
|
560
|
+
raw = await readFile(credPath, 'utf-8');
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
throw new Error(`ADC credentials file not found at ${credPath}. ` +
|
|
564
|
+
'Run: gcloud auth application-default login --scopes=' +
|
|
565
|
+
'https://www.googleapis.com/auth/chat.bot,' +
|
|
566
|
+
'https://www.googleapis.com/auth/pubsub,' +
|
|
567
|
+
'https://www.googleapis.com/auth/cloud-platform');
|
|
568
|
+
}
|
|
569
|
+
let parsed;
|
|
570
|
+
try {
|
|
571
|
+
parsed = JSON.parse(raw);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
throw new Error('ADC credentials file is not valid JSON');
|
|
575
|
+
}
|
|
576
|
+
if (!parsed.client_id || !parsed.client_secret || !parsed.refresh_token) {
|
|
577
|
+
throw new Error('ADC credentials file must contain client_id, client_secret, and refresh_token. ' +
|
|
578
|
+
'Ensure you ran gcloud auth application-default login with the correct scopes.');
|
|
579
|
+
}
|
|
580
|
+
return {
|
|
581
|
+
client_id: parsed.client_id,
|
|
582
|
+
client_secret: parsed.client_secret,
|
|
583
|
+
refresh_token: parsed.refresh_token,
|
|
584
|
+
type: parsed.type || 'authorized_user',
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Reset all internal state to defaults.
|
|
589
|
+
*/
|
|
590
|
+
resetState() {
|
|
591
|
+
this.stopPubSubPull();
|
|
592
|
+
this.webhookUrl = null;
|
|
593
|
+
this.serviceAccountKey = null;
|
|
594
|
+
this.authMode = 'service_account';
|
|
595
|
+
this.adcCredentials = null;
|
|
596
|
+
this.accessToken = null;
|
|
597
|
+
this.tokenExpiresAt = 0;
|
|
598
|
+
this.tokenScopes = GOOGLE_CHAT_PUBSUB_CONSTANTS.CHAT_SCOPE;
|
|
599
|
+
this.pendingTokenRefresh = null;
|
|
600
|
+
this.subscriptionName = null;
|
|
601
|
+
this.projectId = null;
|
|
602
|
+
this.onIncomingMessage = null;
|
|
603
|
+
this.consecutiveFailures = 0;
|
|
604
|
+
this.pullPaused = false;
|
|
605
|
+
this.mode = 'none';
|
|
606
|
+
}
|
|
232
607
|
}
|
|
233
608
|
//# sourceMappingURL=google-chat-messenger.adapter.js.map
|