@checkstack/integration-webex-backend 0.0.2

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 ADDED
@@ -0,0 +1,33 @@
1
+ # @checkstack/integration-webex-backend
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/backend-api@0.0.2
10
+ - @checkstack/common@0.0.2
11
+ - @checkstack/integration-backend@0.0.2
12
+
13
+ ## 0.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - 4c5aa9e: Add Webex integration provider - sends events to Webex team spaces
18
+
19
+ - Connection schema for admin-configured Bot Token
20
+ - Dynamic room/space selection via Webex API
21
+ - Template-based message formatting with default fallback
22
+ - Test connection verification via /people/me endpoint
23
+ - Comprehensive documentation for bot setup
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [4c5aa9e]
28
+ - Updated dependencies [b4eb432]
29
+ - Updated dependencies [a65e002]
30
+ - Updated dependencies [a65e002]
31
+ - @checkstack/integration-backend@0.1.0
32
+ - @checkstack/backend-api@1.1.0
33
+ - @checkstack/common@0.2.0
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@checkstack/integration-webex-backend",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkstack/backend-api": "workspace:*",
13
+ "@checkstack/integration-backend": "workspace:*",
14
+ "@checkstack/common": "workspace:*",
15
+ "zod": "^4.2.1"
16
+ },
17
+ "devDependencies": {
18
+ "@types/bun": "^1.0.0",
19
+ "typescript": "^5.0.0",
20
+ "@checkstack/tsconfig": "workspace:*"
21
+ }
22
+ }
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { createBackendPlugin } from "@checkstack/backend-api";
2
+ import { providerExtensionPoint } from "@checkstack/integration-backend";
3
+ import { pluginMetadata } from "./plugin-metadata";
4
+ import { webexProvider } from "./provider";
5
+
6
+ export default createBackendPlugin({
7
+ metadata: pluginMetadata,
8
+
9
+ register(env) {
10
+ // Get the integration provider extension point
11
+ const extensionPoint = env.getExtensionPoint(providerExtensionPoint);
12
+
13
+ // Register the Webex provider with our plugin metadata
14
+ extensionPoint.addProvider(webexProvider, pluginMetadata);
15
+ },
16
+ });
17
+
18
+ // Re-export for testing
19
+ export {
20
+ webexProvider,
21
+ WebexConnectionSchema,
22
+ WebexSubscriptionSchema,
23
+ } from "./provider";
@@ -0,0 +1,5 @@
1
+ import type { PluginMetadata } from "@checkstack/common";
2
+
3
+ export const pluginMetadata: PluginMetadata = {
4
+ pluginId: "integration-webex",
5
+ };
@@ -0,0 +1,373 @@
1
+ import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test";
2
+ import {
3
+ webexProvider,
4
+ WebexConnectionSchema,
5
+ WebexSubscriptionSchema,
6
+ } from "./index";
7
+
8
+ /**
9
+ * Unit tests for the Webex Integration Provider.
10
+ *
11
+ * Tests cover:
12
+ * - Config schema validation
13
+ * - Connection testing
14
+ * - Room options resolution
15
+ * - Event delivery
16
+ */
17
+
18
+ // Mock logger
19
+ const mockLogger = {
20
+ debug: mock(() => {}),
21
+ info: mock(() => {}),
22
+ warn: mock(() => {}),
23
+ error: mock(() => {}),
24
+ };
25
+
26
+ describe("Webex Integration Provider", () => {
27
+ beforeEach(() => {
28
+ mockLogger.debug.mockClear();
29
+ mockLogger.info.mockClear();
30
+ mockLogger.warn.mockClear();
31
+ mockLogger.error.mockClear();
32
+ });
33
+
34
+ // ─────────────────────────────────────────────────────────────────────────
35
+ // Provider Metadata
36
+ // ─────────────────────────────────────────────────────────────────────────
37
+
38
+ describe("metadata", () => {
39
+ it("has correct basic metadata", () => {
40
+ expect(webexProvider.id).toBe("webex");
41
+ expect(webexProvider.displayName).toBe("Webex");
42
+ expect(webexProvider.description).toContain("Webex");
43
+ expect(webexProvider.icon).toBe("MessageSquare");
44
+ });
45
+
46
+ it("has versioned config and connection schemas", () => {
47
+ expect(webexProvider.config).toBeDefined();
48
+ expect(webexProvider.config.version).toBe(1);
49
+ expect(webexProvider.connectionSchema).toBeDefined();
50
+ expect(webexProvider.connectionSchema?.version).toBe(1);
51
+ });
52
+
53
+ it("has documentation", () => {
54
+ expect(webexProvider.documentation).toBeDefined();
55
+ expect(webexProvider.documentation?.setupGuide).toContain("Webex Bot");
56
+ });
57
+ });
58
+
59
+ // ─────────────────────────────────────────────────────────────────────────
60
+ // Config Schema Validation
61
+ // ─────────────────────────────────────────────────────────────────────────
62
+
63
+ describe("connection schema", () => {
64
+ it("requires bot token", () => {
65
+ expect(() => {
66
+ WebexConnectionSchema.parse({});
67
+ }).toThrow();
68
+ });
69
+
70
+ it("accepts valid connection config", () => {
71
+ const result = WebexConnectionSchema.parse({
72
+ botToken: "test-bot-token-abc123",
73
+ });
74
+ expect(result.botToken).toBe("test-bot-token-abc123");
75
+ });
76
+ });
77
+
78
+ describe("subscription schema", () => {
79
+ it("requires connectionId and roomId", () => {
80
+ expect(() => {
81
+ WebexSubscriptionSchema.parse({});
82
+ }).toThrow();
83
+
84
+ expect(() => {
85
+ WebexSubscriptionSchema.parse({ connectionId: "conn-1" });
86
+ }).toThrow();
87
+ });
88
+
89
+ it("accepts valid subscription config", () => {
90
+ const result = WebexSubscriptionSchema.parse({
91
+ connectionId: "conn-1",
92
+ roomId: "room-123",
93
+ });
94
+ expect(result.connectionId).toBe("conn-1");
95
+ expect(result.roomId).toBe("room-123");
96
+ expect(result.messageTemplate).toBeUndefined();
97
+ });
98
+
99
+ it("accepts optional message template", () => {
100
+ const result = WebexSubscriptionSchema.parse({
101
+ connectionId: "conn-1",
102
+ roomId: "room-123",
103
+ messageTemplate: "Event: {{event.eventId}}",
104
+ });
105
+ expect(result.messageTemplate).toBe("Event: {{event.eventId}}");
106
+ });
107
+ });
108
+
109
+ // ─────────────────────────────────────────────────────────────────────────
110
+ // Test Connection
111
+ // ─────────────────────────────────────────────────────────────────────────
112
+
113
+ describe("testConnection", () => {
114
+ it("returns success for valid token", async () => {
115
+ const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
116
+ (async () => {
117
+ return new Response(
118
+ JSON.stringify({ id: "bot-123", displayName: "Test Bot" }),
119
+ { status: 200 }
120
+ );
121
+ }) as unknown as typeof fetch
122
+ );
123
+
124
+ try {
125
+ const result = await webexProvider.testConnection!({
126
+ botToken: "valid-token",
127
+ });
128
+
129
+ expect(result.success).toBe(true);
130
+ expect(result.message).toContain("Test Bot");
131
+ } finally {
132
+ mockFetch.mockRestore();
133
+ }
134
+ });
135
+
136
+ it("returns failure for invalid token", async () => {
137
+ const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
138
+ (async () => {
139
+ return new Response("Unauthorized", { status: 401 });
140
+ }) as unknown as typeof fetch
141
+ );
142
+
143
+ try {
144
+ const result = await webexProvider.testConnection!({
145
+ botToken: "invalid-token",
146
+ });
147
+
148
+ expect(result.success).toBe(false);
149
+ expect(result.message).toContain("failed");
150
+ } finally {
151
+ mockFetch.mockRestore();
152
+ }
153
+ });
154
+
155
+ it("returns failure for invalid config", async () => {
156
+ // Pass config with empty botToken - passes validation but fails API call
157
+ const result = await webexProvider.testConnection!({
158
+ botToken: "",
159
+ });
160
+
161
+ expect(result.success).toBe(false);
162
+ expect(result.message).toContain("failed");
163
+ });
164
+ });
165
+
166
+ // ─────────────────────────────────────────────────────────────────────────
167
+ // Get Connection Options
168
+ // ─────────────────────────────────────────────────────────────────────────
169
+
170
+ describe("getConnectionOptions", () => {
171
+ it("returns room options when resolver matches", async () => {
172
+ const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
173
+ (async () => {
174
+ return new Response(
175
+ JSON.stringify({
176
+ items: [
177
+ { id: "room-1", title: "Engineering", type: "group" },
178
+ { id: "room-2", title: "DevOps", type: "group" },
179
+ ],
180
+ }),
181
+ { status: 200 }
182
+ );
183
+ }) as unknown as typeof fetch
184
+ );
185
+
186
+ try {
187
+ const options = await webexProvider.getConnectionOptions!({
188
+ resolverName: "roomOptions",
189
+ connectionId: "conn-1",
190
+ context: {},
191
+ logger: mockLogger,
192
+ getConnectionWithCredentials: async () => ({
193
+ config: { botToken: "test-token" },
194
+ }),
195
+ });
196
+
197
+ expect(options).toHaveLength(2);
198
+ expect(options[0]).toEqual({ value: "room-1", label: "Engineering" });
199
+ expect(options[1]).toEqual({ value: "room-2", label: "DevOps" });
200
+ } finally {
201
+ mockFetch.mockRestore();
202
+ }
203
+ });
204
+
205
+ it("returns empty array for unknown resolver", async () => {
206
+ const options = await webexProvider.getConnectionOptions!({
207
+ resolverName: "unknownResolver",
208
+ connectionId: "conn-1",
209
+ context: {},
210
+ logger: mockLogger,
211
+ getConnectionWithCredentials: async () => ({
212
+ config: { botToken: "test-token" },
213
+ }),
214
+ });
215
+
216
+ expect(options).toEqual([]);
217
+ });
218
+
219
+ it("returns empty array when connection not found", async () => {
220
+ const options = await webexProvider.getConnectionOptions!({
221
+ resolverName: "roomOptions",
222
+ connectionId: "conn-1",
223
+ context: {},
224
+ logger: mockLogger,
225
+ getConnectionWithCredentials: async () => undefined,
226
+ });
227
+
228
+ expect(options).toEqual([]);
229
+ });
230
+ });
231
+
232
+ // ─────────────────────────────────────────────────────────────────────────
233
+ // Delivery
234
+ // ─────────────────────────────────────────────────────────────────────────
235
+
236
+ describe("deliver", () => {
237
+ it("sends message to Webex room successfully", async () => {
238
+ let capturedBody: string | undefined;
239
+
240
+ const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
241
+ _url: RequestInfo | URL,
242
+ options?: RequestInit
243
+ ) => {
244
+ capturedBody = options?.body as string;
245
+ return new Response(JSON.stringify({ id: "msg-456" }), {
246
+ status: 200,
247
+ });
248
+ }) as unknown as typeof fetch);
249
+
250
+ try {
251
+ const result = await webexProvider.deliver({
252
+ event: {
253
+ eventId: "incident.created",
254
+ payload: { incidentId: "inc-123", title: "Server Down" },
255
+ timestamp: new Date().toISOString(),
256
+ deliveryId: "del-789",
257
+ },
258
+ subscription: {
259
+ id: "sub-1",
260
+ name: "Incident Notifications",
261
+ },
262
+ providerConfig: {
263
+ connectionId: "conn-1",
264
+ roomId: "room-123",
265
+ },
266
+ logger: mockLogger,
267
+ getConnectionWithCredentials: async () => ({
268
+ id: "conn-1",
269
+ config: { botToken: "test-token" },
270
+ }),
271
+ });
272
+
273
+ expect(result.success).toBe(true);
274
+ expect(result.externalId).toBe("msg-456");
275
+
276
+ const parsedBody = JSON.parse(capturedBody!);
277
+ expect(parsedBody.roomId).toBe("room-123");
278
+ expect(parsedBody.markdown).toContain("incident.created");
279
+ } finally {
280
+ mockFetch.mockRestore();
281
+ }
282
+ });
283
+
284
+ it("uses custom message template when provided", async () => {
285
+ let capturedBody: string | undefined;
286
+
287
+ const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
288
+ _url: RequestInfo | URL,
289
+ options?: RequestInit
290
+ ) => {
291
+ capturedBody = options?.body as string;
292
+ return new Response(JSON.stringify({ id: "msg-456" }), {
293
+ status: 200,
294
+ });
295
+ }) as unknown as typeof fetch);
296
+
297
+ try {
298
+ await webexProvider.deliver({
299
+ event: {
300
+ eventId: "incident.created",
301
+ payload: { incidentId: "inc-123", title: "Server Down" },
302
+ timestamp: new Date().toISOString(),
303
+ deliveryId: "del-789",
304
+ },
305
+ subscription: {
306
+ id: "sub-1",
307
+ name: "Test Sub",
308
+ },
309
+ providerConfig: {
310
+ connectionId: "conn-1",
311
+ roomId: "room-123",
312
+ messageTemplate:
313
+ "🚨 **{{event.payload.title}}** - Incident {{event.payload.incidentId}}",
314
+ },
315
+ logger: mockLogger,
316
+ getConnectionWithCredentials: async () => ({
317
+ id: "conn-1",
318
+ config: { botToken: "test-token" },
319
+ }),
320
+ });
321
+
322
+ const parsedBody = JSON.parse(capturedBody!);
323
+ expect(parsedBody.markdown).toBe(
324
+ "🚨 **Server Down** - Incident inc-123"
325
+ );
326
+ } finally {
327
+ mockFetch.mockRestore();
328
+ }
329
+ });
330
+
331
+ it("returns error when connection not found", async () => {
332
+ const result = await webexProvider.deliver({
333
+ event: {
334
+ eventId: "test.event",
335
+ payload: {},
336
+ timestamp: new Date().toISOString(),
337
+ deliveryId: "del-1",
338
+ },
339
+ subscription: { id: "sub-1", name: "Test" },
340
+ providerConfig: {
341
+ connectionId: "nonexistent",
342
+ roomId: "room-1",
343
+ },
344
+ logger: mockLogger,
345
+ getConnectionWithCredentials: async () => undefined,
346
+ });
347
+
348
+ expect(result.success).toBe(false);
349
+ expect(result.error).toContain("not found");
350
+ });
351
+
352
+ it("returns error when credentials not available", async () => {
353
+ const result = await webexProvider.deliver({
354
+ event: {
355
+ eventId: "test.event",
356
+ payload: {},
357
+ timestamp: new Date().toISOString(),
358
+ deliveryId: "del-1",
359
+ },
360
+ subscription: { id: "sub-1", name: "Test" },
361
+ providerConfig: {
362
+ connectionId: "conn-1",
363
+ roomId: "room-1",
364
+ },
365
+ logger: mockLogger,
366
+ // getConnectionWithCredentials not provided
367
+ });
368
+
369
+ expect(result.success).toBe(false);
370
+ expect(result.error).toContain("not available");
371
+ });
372
+ });
373
+ });
@@ -0,0 +1,383 @@
1
+ import { z } from "zod";
2
+ import { configString, Versioned } from "@checkstack/backend-api";
3
+ import type {
4
+ IntegrationProvider,
5
+ IntegrationDeliveryContext,
6
+ IntegrationDeliveryResult,
7
+ GetConnectionOptionsParams,
8
+ ConnectionOption,
9
+ TestConnectionResult,
10
+ } from "@checkstack/integration-backend";
11
+
12
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13
+ // Resolver Names
14
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
15
+
16
+ const WEBEX_RESOLVERS = {
17
+ ROOM_OPTIONS: "roomOptions",
18
+ } as const;
19
+
20
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
21
+ // Configuration Schemas
22
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23
+
24
+ /**
25
+ * Connection configuration - site-wide Webex Bot credentials.
26
+ */
27
+ export const WebexConnectionSchema = z.object({
28
+ botToken: configString({ "x-secret": true }).describe(
29
+ "Webex Bot Access Token from developer.webex.com"
30
+ ),
31
+ });
32
+
33
+ export type WebexConnectionConfig = z.infer<typeof WebexConnectionSchema>;
34
+
35
+ /**
36
+ * Subscription configuration - which Webex room to send events to.
37
+ */
38
+ export const WebexSubscriptionSchema = z.object({
39
+ connectionId: configString({ "x-hidden": true }).describe("Webex connection"),
40
+ roomId: configString({
41
+ "x-options-resolver": WEBEX_RESOLVERS.ROOM_OPTIONS,
42
+ "x-depends-on": ["connectionId"],
43
+ }).describe("Target Webex Space"),
44
+ messageTemplate: z
45
+ .string()
46
+ .optional()
47
+ .describe(
48
+ "Message template (supports {{event.payload.*}} placeholders). Leave empty for default format."
49
+ ),
50
+ });
51
+
52
+ export type WebexSubscriptionConfig = z.infer<typeof WebexSubscriptionSchema>;
53
+
54
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
55
+ // Webex API Client
56
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
57
+
58
+ const WEBEX_API_BASE = "https://webexapis.com/v1";
59
+
60
+ interface WebexRoom {
61
+ id: string;
62
+ title: string;
63
+ type: "direct" | "group";
64
+ }
65
+
66
+ interface WebexRoomsResponse {
67
+ items: WebexRoom[];
68
+ }
69
+
70
+ interface WebexMeResponse {
71
+ id: string;
72
+ displayName: string;
73
+ }
74
+
75
+ interface WebexMessageResponse {
76
+ id: string;
77
+ }
78
+
79
+ async function fetchWebexRooms(
80
+ botToken: string
81
+ ): Promise<
82
+ { success: true; rooms: WebexRoom[] } | { success: false; error: string }
83
+ > {
84
+ try {
85
+ const response = await fetch(`${WEBEX_API_BASE}/rooms?type=group&max=100`, {
86
+ headers: {
87
+ Authorization: `Bearer ${botToken}`,
88
+ },
89
+ signal: AbortSignal.timeout(10_000),
90
+ });
91
+
92
+ if (!response.ok) {
93
+ return { success: false, error: `Webex API error: ${response.status}` };
94
+ }
95
+
96
+ const data = (await response.json()) as WebexRoomsResponse;
97
+ return { success: true, rooms: data.items ?? [] };
98
+ } catch (error) {
99
+ const message = error instanceof Error ? error.message : "Unknown error";
100
+ return { success: false, error: message };
101
+ }
102
+ }
103
+
104
+ async function sendWebexMessage(params: {
105
+ botToken: string;
106
+ roomId: string;
107
+ markdown: string;
108
+ }): Promise<
109
+ { success: true; messageId: string } | { success: false; error: string }
110
+ > {
111
+ try {
112
+ const response = await fetch(`${WEBEX_API_BASE}/messages`, {
113
+ method: "POST",
114
+ headers: {
115
+ Authorization: `Bearer ${params.botToken}`,
116
+ "Content-Type": "application/json",
117
+ },
118
+ body: JSON.stringify({
119
+ roomId: params.roomId,
120
+ markdown: params.markdown,
121
+ }),
122
+ signal: AbortSignal.timeout(10_000),
123
+ });
124
+
125
+ if (!response.ok) {
126
+ const errorText = await response.text();
127
+ return {
128
+ success: false,
129
+ error: `Webex API error (${response.status}): ${errorText.slice(
130
+ 0,
131
+ 200
132
+ )}`,
133
+ };
134
+ }
135
+
136
+ const data = (await response.json()) as WebexMessageResponse;
137
+ return { success: true, messageId: data.id };
138
+ } catch (error) {
139
+ const message = error instanceof Error ? error.message : "Unknown error";
140
+ return { success: false, error: message };
141
+ }
142
+ }
143
+
144
+ async function testWebexConnection(
145
+ botToken: string
146
+ ): Promise<
147
+ { success: true; botName: string } | { success: false; error: string }
148
+ > {
149
+ try {
150
+ const response = await fetch(`${WEBEX_API_BASE}/people/me`, {
151
+ headers: {
152
+ Authorization: `Bearer ${botToken}`,
153
+ },
154
+ signal: AbortSignal.timeout(10_000),
155
+ });
156
+
157
+ if (!response.ok) {
158
+ return { success: false, error: `Webex API error: ${response.status}` };
159
+ }
160
+
161
+ const data = (await response.json()) as WebexMeResponse;
162
+ return { success: true, botName: data.displayName };
163
+ } catch (error) {
164
+ const message = error instanceof Error ? error.message : "Unknown error";
165
+ return { success: false, error: message };
166
+ }
167
+ }
168
+
169
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
170
+ // Template Expansion
171
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
172
+
173
+ function expandTemplate(
174
+ template: string,
175
+ context: Record<string, unknown>
176
+ ): string {
177
+ return template.replaceAll(/\{\{([^}]+)\}\}/g, (_match, path: string) => {
178
+ const trimmedPath = path.trim();
179
+ const parts = trimmedPath.split(".");
180
+ let value: unknown = context;
181
+ for (const part of parts) {
182
+ if (value === null || value === undefined) {
183
+ return "";
184
+ }
185
+ value = (value as Record<string, unknown>)[part];
186
+ }
187
+ if (value === null || value === undefined) {
188
+ return "";
189
+ }
190
+ if (typeof value === "object") {
191
+ return JSON.stringify(value);
192
+ }
193
+ return String(value);
194
+ });
195
+ }
196
+
197
+ function buildDefaultMessage(
198
+ eventId: string,
199
+ payload: Record<string, unknown>,
200
+ subscriptionName: string
201
+ ): string {
202
+ const lines: string[] = [
203
+ `📢 **Integration Event**`,
204
+ `**Event:** ${eventId}`,
205
+ `**Subscription:** ${subscriptionName}`,
206
+ ``,
207
+ `**Payload:**`,
208
+ "```json",
209
+ JSON.stringify(payload, undefined, 2),
210
+ "```",
211
+ ];
212
+ return lines.join("\n");
213
+ }
214
+
215
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
216
+ // Provider Implementation
217
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
218
+
219
+ export const webexProvider: IntegrationProvider<
220
+ WebexSubscriptionConfig,
221
+ WebexConnectionConfig
222
+ > = {
223
+ id: "webex",
224
+ displayName: "Webex",
225
+ description: "Send integration events to Webex team spaces",
226
+ icon: "MessageSquare",
227
+
228
+ config: new Versioned({
229
+ version: 1,
230
+ schema: WebexSubscriptionSchema,
231
+ }),
232
+
233
+ connectionSchema: new Versioned({
234
+ version: 1,
235
+ schema: WebexConnectionSchema,
236
+ }),
237
+
238
+ documentation: {
239
+ setupGuide: `
240
+ ## Create a Webex Bot
241
+
242
+ 1. Go to [developer.webex.com](https://developer.webex.com/) and sign in
243
+ 2. Navigate to **My Webex Apps** → **Create a New App** → **Create a Bot**
244
+ 3. Fill in the bot details and create
245
+ 4. Copy the **Bot Access Token** (shown only once)
246
+
247
+ ## Add Bot to Spaces
248
+
249
+ 1. In the Webex app, open the space where you want to receive events
250
+ 2. Click the **Add People** button
251
+ 3. Search for your bot's username and add it
252
+
253
+ > **Note**: The bot must be a member of a space to send messages there.
254
+ `.trim(),
255
+ },
256
+
257
+ async getConnectionOptions(
258
+ params: GetConnectionOptionsParams
259
+ ): Promise<ConnectionOption[]> {
260
+ const { resolverName, connectionId, getConnectionWithCredentials } = params;
261
+
262
+ if (resolverName !== WEBEX_RESOLVERS.ROOM_OPTIONS) {
263
+ return [];
264
+ }
265
+
266
+ // Get connection credentials
267
+ const connection = await getConnectionWithCredentials(connectionId);
268
+ if (!connection) {
269
+ return [];
270
+ }
271
+
272
+ const config = connection.config as WebexConnectionConfig;
273
+ const result = await fetchWebexRooms(config.botToken);
274
+
275
+ if (!result.success) {
276
+ return [];
277
+ }
278
+
279
+ return result.rooms.map((room) => ({
280
+ value: room.id,
281
+ label: room.title,
282
+ }));
283
+ },
284
+
285
+ async testConnection(config: unknown): Promise<TestConnectionResult> {
286
+ try {
287
+ const parsedConfig = WebexConnectionSchema.parse(config);
288
+ const result = await testWebexConnection(parsedConfig.botToken);
289
+
290
+ return result.success
291
+ ? {
292
+ success: true,
293
+ message: `Connected as bot: ${result.botName}`,
294
+ }
295
+ : {
296
+ success: false,
297
+ message: `Connection failed: ${result.error}`,
298
+ };
299
+ } catch (error) {
300
+ const message =
301
+ error instanceof Error ? error.message : "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
+ return {
378
+ success: false,
379
+ error: result.error,
380
+ };
381
+ }
382
+ },
383
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "@checkstack/tsconfig/backend.json"
3
+ }