@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 +33 -0
- package/package.json +22 -0
- package/src/index.ts +23 -0
- package/src/plugin-metadata.ts +5 -0
- package/src/provider.test.ts +373 -0
- package/src/provider.ts +383 -0
- package/tsconfig.json +3 -0
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,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
|
+
});
|
package/src/provider.ts
ADDED
|
@@ -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