@checkstack/integration-webex-backend 0.0.34 → 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/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
- const WEBEX_RESOLVERS = {
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
- const message = extractErrorMessage(error, "Unknown error");
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
- const message = extractErrorMessage(error, "Unknown error");
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
- function buildDefaultMessage(
199
- eventId: string,
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 integration events to Webex team spaces",
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 events
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
- if (!result.success) {
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: unknown): Promise<TestConnectionResult> {
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
- success: true,
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
- error: result.error,
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
  }