@checkstack/integration-webex-backend 0.0.35 → 0.1.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 +199 -0
- package/package.json +7 -4
- package/src/automations.test.ts +199 -0
- package/src/automations.ts +138 -0
- package/src/index.ts +27 -6
- package/src/provider.ts +44 -272
- package/tsconfig.json +6 -0
- package/src/provider.test.ts +0 -383
package/src/provider.ts
CHANGED
|
@@ -2,29 +2,41 @@ import { z } from "zod";
|
|
|
2
2
|
import { configString, Versioned } from "@checkstack/backend-api";
|
|
3
3
|
import type {
|
|
4
4
|
IntegrationProvider,
|
|
5
|
-
IntegrationDeliveryContext,
|
|
6
|
-
IntegrationDeliveryResult,
|
|
7
5
|
GetConnectionOptionsParams,
|
|
8
6
|
ConnectionOption,
|
|
9
7
|
TestConnectionResult,
|
|
10
8
|
} from "@checkstack/integration-backend";
|
|
11
9
|
import { extractErrorMessage } from "@checkstack/common";
|
|
10
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
12
11
|
|
|
13
|
-
//
|
|
14
|
-
// Resolver Names
|
|
15
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
12
|
+
// ─── Provider id ─────────────────────────────────────────────────────────
|
|
16
13
|
|
|
17
|
-
|
|
14
|
+
/** Local provider id (namespaced on registration to `{pluginId}.{id}`). */
|
|
15
|
+
export const WEBEX_PROVIDER_LOCAL_ID = "webex";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fully-qualified Webex provider id (`integration-webex.webex`). Derived from
|
|
19
|
+
* the plugin's own `pluginMetadata` so it tracks the plugin id rather than a
|
|
20
|
+
* hardcoded string. Automation actions set this as `connectionProviderId` so
|
|
21
|
+
* the editor knows which integration provider backs their dropdowns, and it
|
|
22
|
+
* matches the `qualifiedId` the integration provider registry assigns.
|
|
23
|
+
*/
|
|
24
|
+
export const WEBEX_PROVIDER_QUALIFIED_ID = `${pluginMetadata.pluginId}.${WEBEX_PROVIDER_LOCAL_ID}`;
|
|
25
|
+
|
|
26
|
+
// ─── Resolver names ─────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export const WEBEX_RESOLVERS = {
|
|
29
|
+
/**
|
|
30
|
+
* Site-wide Webex connections. Drives the connection picker on the Webex
|
|
31
|
+
* action; the editor bridge resolves it via `listConnections` (no
|
|
32
|
+
* connection is selected yet), not `getConnectionOptions`.
|
|
33
|
+
*/
|
|
34
|
+
CONNECTION_OPTIONS: "connectionOptions",
|
|
18
35
|
ROOM_OPTIONS: "roomOptions",
|
|
19
36
|
} as const;
|
|
20
37
|
|
|
21
|
-
//
|
|
22
|
-
// Configuration Schemas
|
|
23
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
// ─── Connection schema ──────────────────────────────────────────────────
|
|
24
39
|
|
|
25
|
-
/**
|
|
26
|
-
* Connection configuration - site-wide Webex Bot credentials.
|
|
27
|
-
*/
|
|
28
40
|
export const WebexConnectionSchema = z.object({
|
|
29
41
|
botToken: configString({ "x-secret": true }).describe(
|
|
30
42
|
"Webex Bot Access Token from developer.webex.com",
|
|
@@ -33,28 +45,7 @@ export const WebexConnectionSchema = z.object({
|
|
|
33
45
|
|
|
34
46
|
export type WebexConnectionConfig = z.infer<typeof WebexConnectionSchema>;
|
|
35
47
|
|
|
36
|
-
|
|
37
|
-
* Subscription configuration - which Webex room to send events to.
|
|
38
|
-
*/
|
|
39
|
-
export const WebexSubscriptionSchema = z.object({
|
|
40
|
-
connectionId: configString({ "x-hidden": true }).describe("Webex connection"),
|
|
41
|
-
roomId: configString({
|
|
42
|
-
"x-options-resolver": WEBEX_RESOLVERS.ROOM_OPTIONS,
|
|
43
|
-
"x-depends-on": ["connectionId"],
|
|
44
|
-
}).describe("Target Webex Space"),
|
|
45
|
-
messageTemplate: z
|
|
46
|
-
.string()
|
|
47
|
-
.optional()
|
|
48
|
-
.describe(
|
|
49
|
-
"Message template (supports {{event.payload.*}} placeholders). Leave empty for default format.",
|
|
50
|
-
),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
export type WebexSubscriptionConfig = z.infer<typeof WebexSubscriptionSchema>;
|
|
54
|
-
|
|
55
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
56
|
-
// Webex API Client
|
|
57
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
48
|
+
// ─── Webex API helpers ──────────────────────────────────────────────────
|
|
58
49
|
|
|
59
50
|
const WEBEX_API_BASE = "https://webexapis.com/v1";
|
|
60
51
|
|
|
@@ -64,19 +55,6 @@ interface WebexRoom {
|
|
|
64
55
|
type: "direct" | "group";
|
|
65
56
|
}
|
|
66
57
|
|
|
67
|
-
interface WebexRoomsResponse {
|
|
68
|
-
items: WebexRoom[];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
interface WebexMeResponse {
|
|
72
|
-
id: string;
|
|
73
|
-
displayName: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface WebexMessageResponse {
|
|
77
|
-
id: string;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
58
|
async function fetchWebexRooms(
|
|
81
59
|
botToken: string,
|
|
82
60
|
): Promise<
|
|
@@ -84,61 +62,16 @@ async function fetchWebexRooms(
|
|
|
84
62
|
> {
|
|
85
63
|
try {
|
|
86
64
|
const response = await fetch(`${WEBEX_API_BASE}/rooms?type=group&max=100`, {
|
|
87
|
-
headers: {
|
|
88
|
-
Authorization: `Bearer ${botToken}`,
|
|
89
|
-
},
|
|
65
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
90
66
|
signal: AbortSignal.timeout(10_000),
|
|
91
67
|
});
|
|
92
|
-
|
|
93
68
|
if (!response.ok) {
|
|
94
69
|
return { success: false, error: `Webex API error: ${response.status}` };
|
|
95
70
|
}
|
|
96
|
-
|
|
97
|
-
const data = (await response.json()) as WebexRoomsResponse;
|
|
71
|
+
const data = (await response.json()) as { items: WebexRoom[] };
|
|
98
72
|
return { success: true, rooms: data.items ?? [] };
|
|
99
73
|
} catch (error) {
|
|
100
|
-
|
|
101
|
-
return { success: false, error: message };
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
async function sendWebexMessage(params: {
|
|
106
|
-
botToken: string;
|
|
107
|
-
roomId: string;
|
|
108
|
-
markdown: string;
|
|
109
|
-
}): Promise<
|
|
110
|
-
{ success: true; messageId: string } | { success: false; error: string }
|
|
111
|
-
> {
|
|
112
|
-
try {
|
|
113
|
-
const response = await fetch(`${WEBEX_API_BASE}/messages`, {
|
|
114
|
-
method: "POST",
|
|
115
|
-
headers: {
|
|
116
|
-
Authorization: `Bearer ${params.botToken}`,
|
|
117
|
-
"Content-Type": "application/json",
|
|
118
|
-
},
|
|
119
|
-
body: JSON.stringify({
|
|
120
|
-
roomId: params.roomId,
|
|
121
|
-
markdown: params.markdown,
|
|
122
|
-
}),
|
|
123
|
-
signal: AbortSignal.timeout(10_000),
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
if (!response.ok) {
|
|
127
|
-
const errorText = await response.text();
|
|
128
|
-
return {
|
|
129
|
-
success: false,
|
|
130
|
-
error: `Webex API error (${response.status}): ${errorText.slice(
|
|
131
|
-
0,
|
|
132
|
-
200,
|
|
133
|
-
)}`,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const data = (await response.json()) as WebexMessageResponse;
|
|
138
|
-
return { success: true, messageId: data.id };
|
|
139
|
-
} catch (error) {
|
|
140
|
-
const message = extractErrorMessage(error, "Unknown error");
|
|
141
|
-
return { success: false, error: message };
|
|
74
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
142
75
|
}
|
|
143
76
|
}
|
|
144
77
|
|
|
@@ -149,88 +82,27 @@ async function testWebexConnection(
|
|
|
149
82
|
> {
|
|
150
83
|
try {
|
|
151
84
|
const response = await fetch(`${WEBEX_API_BASE}/people/me`, {
|
|
152
|
-
headers: {
|
|
153
|
-
Authorization: `Bearer ${botToken}`,
|
|
154
|
-
},
|
|
85
|
+
headers: { Authorization: `Bearer ${botToken}` },
|
|
155
86
|
signal: AbortSignal.timeout(10_000),
|
|
156
87
|
});
|
|
157
|
-
|
|
158
88
|
if (!response.ok) {
|
|
159
89
|
return { success: false, error: `Webex API error: ${response.status}` };
|
|
160
90
|
}
|
|
161
|
-
|
|
162
|
-
const data = (await response.json()) as WebexMeResponse;
|
|
91
|
+
const data = (await response.json()) as { displayName: string };
|
|
163
92
|
return { success: true, botName: data.displayName };
|
|
164
93
|
} catch (error) {
|
|
165
|
-
|
|
166
|
-
return { success: false, error: message };
|
|
94
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
167
95
|
}
|
|
168
96
|
}
|
|
169
97
|
|
|
170
|
-
//
|
|
171
|
-
// Template Expansion
|
|
172
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
173
|
-
|
|
174
|
-
function expandTemplate(
|
|
175
|
-
template: string,
|
|
176
|
-
context: Record<string, unknown>,
|
|
177
|
-
): string {
|
|
178
|
-
return template.replaceAll(/\{\{([^}]+)\}\}/g, (_match, path: string) => {
|
|
179
|
-
const trimmedPath = path.trim();
|
|
180
|
-
const parts = trimmedPath.split(".");
|
|
181
|
-
let value: unknown = context;
|
|
182
|
-
for (const part of parts) {
|
|
183
|
-
if (value === null || value === undefined) {
|
|
184
|
-
return "";
|
|
185
|
-
}
|
|
186
|
-
value = (value as Record<string, unknown>)[part];
|
|
187
|
-
}
|
|
188
|
-
if (value === null || value === undefined) {
|
|
189
|
-
return "";
|
|
190
|
-
}
|
|
191
|
-
if (typeof value === "object") {
|
|
192
|
-
return JSON.stringify(value);
|
|
193
|
-
}
|
|
194
|
-
return String(value);
|
|
195
|
-
});
|
|
196
|
-
}
|
|
98
|
+
// ─── Provider definition (connection-only) ──────────────────────────────
|
|
197
99
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
payload: Record<string, unknown>,
|
|
201
|
-
subscriptionName: string,
|
|
202
|
-
): string {
|
|
203
|
-
const lines: string[] = [
|
|
204
|
-
`📢 **Integration Event**`,
|
|
205
|
-
`**Event:** ${eventId}`,
|
|
206
|
-
`**Subscription:** ${subscriptionName}`,
|
|
207
|
-
``,
|
|
208
|
-
`**Payload:**`,
|
|
209
|
-
"```json",
|
|
210
|
-
JSON.stringify(payload, undefined, 2),
|
|
211
|
-
"```",
|
|
212
|
-
];
|
|
213
|
-
return lines.join("\n");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
217
|
-
// Provider Implementation
|
|
218
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
219
|
-
|
|
220
|
-
export const webexProvider: IntegrationProvider<
|
|
221
|
-
WebexSubscriptionConfig,
|
|
222
|
-
WebexConnectionConfig
|
|
223
|
-
> = {
|
|
224
|
-
id: "webex",
|
|
100
|
+
export const webexProvider: IntegrationProvider<WebexConnectionConfig> = {
|
|
101
|
+
id: WEBEX_PROVIDER_LOCAL_ID,
|
|
225
102
|
displayName: "Webex",
|
|
226
|
-
description: "Send
|
|
103
|
+
description: "Send automation messages to Webex team spaces",
|
|
227
104
|
icon: "MessageSquare",
|
|
228
105
|
|
|
229
|
-
config: new Versioned({
|
|
230
|
-
version: 1,
|
|
231
|
-
schema: WebexSubscriptionSchema,
|
|
232
|
-
}),
|
|
233
|
-
|
|
234
106
|
connectionSchema: new Versioned({
|
|
235
107
|
version: 1,
|
|
236
108
|
schema: WebexConnectionSchema,
|
|
@@ -247,11 +119,9 @@ export const webexProvider: IntegrationProvider<
|
|
|
247
119
|
|
|
248
120
|
## Add Bot to Spaces
|
|
249
121
|
|
|
250
|
-
1. In the Webex app, open the space where you want to receive
|
|
122
|
+
1. In the Webex app, open the space where you want to receive messages
|
|
251
123
|
2. Click the **Add People** button
|
|
252
124
|
3. Search for your bot's username and add it
|
|
253
|
-
|
|
254
|
-
> **Note**: The bot must be a member of a space to send messages there.
|
|
255
125
|
`.trim(),
|
|
256
126
|
},
|
|
257
127
|
|
|
@@ -259,124 +129,26 @@ export const webexProvider: IntegrationProvider<
|
|
|
259
129
|
params: GetConnectionOptionsParams,
|
|
260
130
|
): Promise<ConnectionOption[]> {
|
|
261
131
|
const { resolverName, connectionId, getConnectionWithCredentials } = params;
|
|
262
|
-
|
|
263
|
-
if (resolverName !== WEBEX_RESOLVERS.ROOM_OPTIONS) {
|
|
264
|
-
return [];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Get connection credentials
|
|
132
|
+
if (resolverName !== WEBEX_RESOLVERS.ROOM_OPTIONS) return [];
|
|
268
133
|
const connection = await getConnectionWithCredentials(connectionId);
|
|
269
|
-
if (!connection)
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
|
|
134
|
+
if (!connection) return [];
|
|
273
135
|
const config = connection.config as WebexConnectionConfig;
|
|
274
136
|
const result = await fetchWebexRooms(config.botToken);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return [];
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return result.rooms.map((room) => ({
|
|
281
|
-
value: room.id,
|
|
282
|
-
label: room.title,
|
|
283
|
-
}));
|
|
137
|
+
if (!result.success) return [];
|
|
138
|
+
return result.rooms.map((room) => ({ value: room.id, label: room.title }));
|
|
284
139
|
},
|
|
285
140
|
|
|
286
|
-
async testConnection(config
|
|
141
|
+
async testConnection(config): Promise<TestConnectionResult> {
|
|
287
142
|
try {
|
|
288
143
|
const parsedConfig = WebexConnectionSchema.parse(config);
|
|
289
144
|
const result = await testWebexConnection(parsedConfig.botToken);
|
|
290
|
-
|
|
291
145
|
return result.success
|
|
292
|
-
? {
|
|
293
|
-
|
|
294
|
-
message: `Connected as bot: ${result.botName}`,
|
|
295
|
-
}
|
|
296
|
-
: {
|
|
297
|
-
success: false,
|
|
298
|
-
message: `Connection failed: ${result.error}`,
|
|
299
|
-
};
|
|
146
|
+
? { success: true, message: `Connected as bot: ${result.botName}` }
|
|
147
|
+
: { success: false, message: `Connection failed: ${result.error}` };
|
|
300
148
|
} catch (error) {
|
|
301
|
-
const message = extractErrorMessage(error, "Invalid configuration");
|
|
302
|
-
return {
|
|
303
|
-
success: false,
|
|
304
|
-
message: `Validation failed: ${message}`,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
},
|
|
308
|
-
|
|
309
|
-
async deliver(
|
|
310
|
-
context: IntegrationDeliveryContext<WebexSubscriptionConfig>,
|
|
311
|
-
): Promise<IntegrationDeliveryResult> {
|
|
312
|
-
const { event, subscription, providerConfig, logger } = context;
|
|
313
|
-
|
|
314
|
-
// Parse and validate config
|
|
315
|
-
const config = WebexSubscriptionSchema.parse(providerConfig);
|
|
316
|
-
|
|
317
|
-
// Get connection with credentials
|
|
318
|
-
if (!context.getConnectionWithCredentials) {
|
|
319
|
-
return {
|
|
320
|
-
success: false,
|
|
321
|
-
error: "Connection credentials not available",
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
const connection = await context.getConnectionWithCredentials(
|
|
326
|
-
config.connectionId,
|
|
327
|
-
);
|
|
328
|
-
|
|
329
|
-
if (!connection) {
|
|
330
|
-
return {
|
|
331
|
-
success: false,
|
|
332
|
-
error: `Connection not found: ${config.connectionId}`,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const connectionConfig = connection.config as WebexConnectionConfig;
|
|
337
|
-
|
|
338
|
-
// Build message
|
|
339
|
-
let markdown: string;
|
|
340
|
-
if (config.messageTemplate) {
|
|
341
|
-
const templateContext = {
|
|
342
|
-
event: {
|
|
343
|
-
eventId: event.eventId,
|
|
344
|
-
payload: event.payload,
|
|
345
|
-
timestamp: event.timestamp,
|
|
346
|
-
deliveryId: event.deliveryId,
|
|
347
|
-
},
|
|
348
|
-
subscription: {
|
|
349
|
-
id: subscription.id,
|
|
350
|
-
name: subscription.name,
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
markdown = expandTemplate(config.messageTemplate, templateContext);
|
|
354
|
-
} else {
|
|
355
|
-
markdown = buildDefaultMessage(
|
|
356
|
-
event.eventId,
|
|
357
|
-
event.payload as Record<string, unknown>,
|
|
358
|
-
subscription.name,
|
|
359
|
-
);
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Send message
|
|
363
|
-
const result = await sendWebexMessage({
|
|
364
|
-
botToken: connectionConfig.botToken,
|
|
365
|
-
roomId: config.roomId,
|
|
366
|
-
markdown,
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (result.success) {
|
|
370
|
-
logger.info("Webex message sent", { messageId: result.messageId });
|
|
371
|
-
return {
|
|
372
|
-
success: true,
|
|
373
|
-
externalId: result.messageId,
|
|
374
|
-
};
|
|
375
|
-
} else {
|
|
376
|
-
logger.error("Failed to send Webex message", { error: result.error });
|
|
377
149
|
return {
|
|
378
150
|
success: false,
|
|
379
|
-
|
|
151
|
+
message: `Validation failed: ${extractErrorMessage(error, "Invalid configuration")}`,
|
|
380
152
|
};
|
|
381
153
|
}
|
|
382
154
|
},
|
package/tsconfig.json
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"extends": "@checkstack/tsconfig/backend.json",
|
|
3
3
|
"references": [
|
|
4
|
+
{
|
|
5
|
+
"path": "../../core/automation-backend"
|
|
6
|
+
},
|
|
4
7
|
{
|
|
5
8
|
"path": "../../core/backend-api"
|
|
6
9
|
},
|
|
@@ -9,6 +12,9 @@
|
|
|
9
12
|
},
|
|
10
13
|
{
|
|
11
14
|
"path": "../../core/integration-backend"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"path": "../../core/test-utils-backend"
|
|
12
18
|
}
|
|
13
19
|
]
|
|
14
20
|
}
|