@checkstack/notification-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 +30 -0
- package/package.json +24 -0
- package/src/index.test.ts +294 -0
- package/src/index.ts +206 -0
- package/src/plugin-metadata.ts +5 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @checkstack/notification-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/notification-backend@0.0.2
|
|
12
|
+
|
|
13
|
+
## 0.1.0
|
|
14
|
+
|
|
15
|
+
### Minor Changes
|
|
16
|
+
|
|
17
|
+
- 4c5aa9e: Add Webex notification strategy - sends alerts to users via Webex direct messages
|
|
18
|
+
|
|
19
|
+
- Bot token configured by admin (long-lived tokens from developer.webex.com)
|
|
20
|
+
- Users configure their Webex Person ID to receive notifications
|
|
21
|
+
- Supports markdown formatting with importance emojis and action links
|
|
22
|
+
- Includes admin and user setup instructions
|
|
23
|
+
|
|
24
|
+
### Patch Changes
|
|
25
|
+
|
|
26
|
+
- Updated dependencies [b4eb432]
|
|
27
|
+
- Updated dependencies [a65e002]
|
|
28
|
+
- @checkstack/backend-api@1.1.0
|
|
29
|
+
- @checkstack/notification-backend@0.1.2
|
|
30
|
+
- @checkstack/common@0.2.0
|
package/package.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/notification-webex-backend",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./plugin-metadata": "./src/plugin-metadata.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"lint": "eslint src --ext .ts"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@checkstack/backend-api": "workspace:*",
|
|
16
|
+
"@checkstack/common": "workspace:*",
|
|
17
|
+
"@checkstack/notification-backend": "workspace:*",
|
|
18
|
+
"zod": "^4.2.1"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
22
|
+
"typescript": "^5.7.2"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, mock, spyOn } from "bun:test";
|
|
2
|
+
import type { Logger } from "@checkstack/backend-api";
|
|
3
|
+
|
|
4
|
+
// Re-export for testing since we can't import directly from index.ts without side effects
|
|
5
|
+
// We'll test the schemas and strategy logic indirectly through the provider
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unit tests for the Webex Notification Strategy.
|
|
9
|
+
*
|
|
10
|
+
* Tests cover:
|
|
11
|
+
* - Config schema validation
|
|
12
|
+
* - Successful message delivery
|
|
13
|
+
* - Error handling
|
|
14
|
+
* - Message formatting
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// Mock logger
|
|
18
|
+
const mockLogger: Logger = {
|
|
19
|
+
debug: mock(() => {}),
|
|
20
|
+
info: mock(() => {}),
|
|
21
|
+
warn: mock(() => {}),
|
|
22
|
+
error: mock(() => {}),
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
describe("Webex Notification Strategy", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
(mockLogger.debug as ReturnType<typeof mock>).mockClear();
|
|
28
|
+
(mockLogger.info as ReturnType<typeof mock>).mockClear();
|
|
29
|
+
(mockLogger.warn as ReturnType<typeof mock>).mockClear();
|
|
30
|
+
(mockLogger.error as ReturnType<typeof mock>).mockClear();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
34
|
+
// Config Schema Validation
|
|
35
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
describe("config schema", () => {
|
|
38
|
+
// Import schemas inline to avoid plugin initialization side effects
|
|
39
|
+
it("validates admin config - requires bot token", async () => {
|
|
40
|
+
const { z } = await import("zod");
|
|
41
|
+
const { configString } = await import("@checkstack/backend-api");
|
|
42
|
+
|
|
43
|
+
const webexConfigSchemaV1 = z.object({
|
|
44
|
+
botToken: configString({ "x-secret": true }).describe(
|
|
45
|
+
"Webex Bot Access Token"
|
|
46
|
+
),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Missing bot token should fail
|
|
50
|
+
expect(() => {
|
|
51
|
+
webexConfigSchemaV1.parse({});
|
|
52
|
+
}).toThrow();
|
|
53
|
+
|
|
54
|
+
// Valid config should pass
|
|
55
|
+
const result = webexConfigSchemaV1.parse({
|
|
56
|
+
botToken: "test-bot-token-123",
|
|
57
|
+
});
|
|
58
|
+
expect(result.botToken).toBe("test-bot-token-123");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("validates user config - requires person ID", async () => {
|
|
62
|
+
const { z } = await import("zod");
|
|
63
|
+
|
|
64
|
+
const webexUserConfigSchema = z.object({
|
|
65
|
+
personId: z.string().min(1).describe("Your Webex Person ID"),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Empty person ID should fail
|
|
69
|
+
expect(() => {
|
|
70
|
+
webexUserConfigSchema.parse({ personId: "" });
|
|
71
|
+
}).toThrow();
|
|
72
|
+
|
|
73
|
+
// Missing person ID should fail
|
|
74
|
+
expect(() => {
|
|
75
|
+
webexUserConfigSchema.parse({});
|
|
76
|
+
}).toThrow();
|
|
77
|
+
|
|
78
|
+
// Valid config should pass
|
|
79
|
+
const result = webexUserConfigSchema.parse({
|
|
80
|
+
personId: "Y2lzY29zcGFyazovL3VzL1BFT1BMRS8xMjM0NTY=",
|
|
81
|
+
});
|
|
82
|
+
expect(result.personId).toBe("Y2lzY29zcGFyazovL3VzL1BFT1BMRS8xMjM0NTY=");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
87
|
+
// Message Delivery
|
|
88
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
describe("message delivery", () => {
|
|
91
|
+
it("sends message with correct payload structure", async () => {
|
|
92
|
+
let capturedBody: string | undefined;
|
|
93
|
+
let capturedHeaders: Record<string, string> | undefined;
|
|
94
|
+
let capturedUrl: string | undefined;
|
|
95
|
+
|
|
96
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
|
|
97
|
+
url: RequestInfo | URL,
|
|
98
|
+
options?: RequestInit
|
|
99
|
+
) => {
|
|
100
|
+
capturedUrl = url.toString();
|
|
101
|
+
capturedBody = options?.body as string;
|
|
102
|
+
capturedHeaders = options?.headers as Record<string, string>;
|
|
103
|
+
return new Response(JSON.stringify({ id: "msg-123" }), {
|
|
104
|
+
status: 200,
|
|
105
|
+
});
|
|
106
|
+
}) as unknown as typeof fetch);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Simulate what the send method does
|
|
110
|
+
const botToken = "test-bot-token";
|
|
111
|
+
const personId = "test-person-id";
|
|
112
|
+
const markdown =
|
|
113
|
+
"ℹ️ **Test Title**\n\nTest body\n\n[View](https://example.com)";
|
|
114
|
+
|
|
115
|
+
const response = await fetch("https://webexapis.com/v1/messages", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${botToken}`,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify({
|
|
122
|
+
toPersonId: personId,
|
|
123
|
+
markdown,
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(capturedUrl).toBe("https://webexapis.com/v1/messages");
|
|
128
|
+
expect(capturedHeaders?.["Authorization"]).toBe(`Bearer ${botToken}`);
|
|
129
|
+
expect(capturedHeaders?.["Content-Type"]).toBe("application/json");
|
|
130
|
+
|
|
131
|
+
const parsedBody = JSON.parse(capturedBody!);
|
|
132
|
+
expect(parsedBody.toPersonId).toBe(personId);
|
|
133
|
+
expect(parsedBody.markdown).toContain("**Test Title**");
|
|
134
|
+
expect(parsedBody.markdown).toContain("Test body");
|
|
135
|
+
expect(parsedBody.markdown).toContain("[View](https://example.com)");
|
|
136
|
+
|
|
137
|
+
expect(response.ok).toBe(true);
|
|
138
|
+
const result = await response.json();
|
|
139
|
+
expect(result.id).toBe("msg-123");
|
|
140
|
+
} finally {
|
|
141
|
+
mockFetch.mockRestore();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("formats messages with correct importance emoji", async () => {
|
|
146
|
+
const importanceEmoji = {
|
|
147
|
+
info: "ℹ️",
|
|
148
|
+
warning: "⚠️",
|
|
149
|
+
critical: "🚨",
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
// Test each importance level
|
|
153
|
+
for (const [importance, emoji] of Object.entries(importanceEmoji)) {
|
|
154
|
+
const title = "Test Alert";
|
|
155
|
+
const markdown = `${emoji} **${title}**`;
|
|
156
|
+
|
|
157
|
+
expect(markdown).toContain(emoji);
|
|
158
|
+
expect(markdown).toContain(`**${title}**`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("handles API errors gracefully", async () => {
|
|
163
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
|
|
164
|
+
(async () => {
|
|
165
|
+
return new Response(
|
|
166
|
+
JSON.stringify({ message: "Invalid personId", trackingId: "123" }),
|
|
167
|
+
{ status: 400 }
|
|
168
|
+
);
|
|
169
|
+
}) as unknown as typeof fetch
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch("https://webexapis.com/v1/messages", {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: {
|
|
176
|
+
Authorization: "Bearer test-token",
|
|
177
|
+
"Content-Type": "application/json",
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify({
|
|
180
|
+
toPersonId: "invalid-person-id",
|
|
181
|
+
markdown: "Test message",
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(response.ok).toBe(false);
|
|
186
|
+
expect(response.status).toBe(400);
|
|
187
|
+
} finally {
|
|
188
|
+
mockFetch.mockRestore();
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("handles network errors", async () => {
|
|
193
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
|
|
194
|
+
(async () => {
|
|
195
|
+
throw new Error("Network error: ECONNREFUSED");
|
|
196
|
+
}) as unknown as typeof fetch
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await expect(
|
|
201
|
+
fetch("https://webexapis.com/v1/messages", {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: {
|
|
204
|
+
Authorization: "Bearer test-token",
|
|
205
|
+
"Content-Type": "application/json",
|
|
206
|
+
},
|
|
207
|
+
body: JSON.stringify({
|
|
208
|
+
toPersonId: "test-person",
|
|
209
|
+
markdown: "Test",
|
|
210
|
+
}),
|
|
211
|
+
})
|
|
212
|
+
).rejects.toThrow("ECONNREFUSED");
|
|
213
|
+
} finally {
|
|
214
|
+
mockFetch.mockRestore();
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it("handles timeout errors", async () => {
|
|
219
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
|
|
220
|
+
(async () => {
|
|
221
|
+
throw new Error("The operation was aborted due to timeout");
|
|
222
|
+
}) as unknown as typeof fetch
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
await expect(
|
|
227
|
+
fetch("https://webexapis.com/v1/messages", {
|
|
228
|
+
method: "POST",
|
|
229
|
+
signal: AbortSignal.timeout(10_000),
|
|
230
|
+
headers: {
|
|
231
|
+
Authorization: "Bearer test-token",
|
|
232
|
+
"Content-Type": "application/json",
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
toPersonId: "test-person",
|
|
236
|
+
markdown: "Test",
|
|
237
|
+
}),
|
|
238
|
+
})
|
|
239
|
+
).rejects.toThrow("timeout");
|
|
240
|
+
} finally {
|
|
241
|
+
mockFetch.mockRestore();
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
247
|
+
// Message Formatting
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
249
|
+
|
|
250
|
+
describe("message formatting", () => {
|
|
251
|
+
it("builds markdown with title only", () => {
|
|
252
|
+
const title = "System Alert";
|
|
253
|
+
const importance = "info" as const;
|
|
254
|
+
const importanceEmoji = { info: "ℹ️", warning: "⚠️", critical: "🚨" };
|
|
255
|
+
|
|
256
|
+
const markdown = `${importanceEmoji[importance]} **${title}**`;
|
|
257
|
+
|
|
258
|
+
expect(markdown).toBe("ℹ️ **System Alert**");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("builds markdown with title and body", () => {
|
|
262
|
+
const title = "System Alert";
|
|
263
|
+
const body = "The system has recovered.";
|
|
264
|
+
const importance = "info" as const;
|
|
265
|
+
const importanceEmoji = { info: "ℹ️", warning: "⚠️", critical: "🚨" };
|
|
266
|
+
|
|
267
|
+
let markdown = `${importanceEmoji[importance]} **${title}**`;
|
|
268
|
+
markdown += `\n\n${body}`;
|
|
269
|
+
|
|
270
|
+
expect(markdown).toBe("ℹ️ **System Alert**\n\nThe system has recovered.");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("builds markdown with action link", () => {
|
|
274
|
+
const title = "Incident Created";
|
|
275
|
+
const body = "A new incident has been reported.";
|
|
276
|
+
const action = {
|
|
277
|
+
label: "View Incident",
|
|
278
|
+
url: "https://example.com/incident/123",
|
|
279
|
+
};
|
|
280
|
+
const importance = "critical" as const;
|
|
281
|
+
const importanceEmoji = { info: "ℹ️", warning: "⚠️", critical: "🚨" };
|
|
282
|
+
|
|
283
|
+
let markdown = `${importanceEmoji[importance]} **${title}**`;
|
|
284
|
+
markdown += `\n\n${body}`;
|
|
285
|
+
markdown += `\n\n[${action.label}](${action.url})`;
|
|
286
|
+
|
|
287
|
+
expect(markdown).toContain("🚨 **Incident Created**");
|
|
288
|
+
expect(markdown).toContain("A new incident has been reported.");
|
|
289
|
+
expect(markdown).toContain(
|
|
290
|
+
"[View Incident](https://example.com/incident/123)"
|
|
291
|
+
);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
createBackendPlugin,
|
|
4
|
+
configString,
|
|
5
|
+
Versioned,
|
|
6
|
+
type NotificationStrategy,
|
|
7
|
+
type NotificationSendContext,
|
|
8
|
+
type NotificationDeliveryResult,
|
|
9
|
+
} from "@checkstack/backend-api";
|
|
10
|
+
import { notificationStrategyExtensionPoint } from "@checkstack/notification-backend";
|
|
11
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
12
|
+
|
|
13
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
14
|
+
// Configuration Schemas
|
|
15
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Admin configuration for Webex strategy.
|
|
19
|
+
* Bot tokens are long-lived (100+ years) so no refresh is needed.
|
|
20
|
+
*/
|
|
21
|
+
const webexConfigSchemaV1 = z.object({
|
|
22
|
+
botToken: configString({ "x-secret": true }).describe(
|
|
23
|
+
"Webex Bot Access Token from developer.webex.com"
|
|
24
|
+
),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
type WebexConfig = z.infer<typeof webexConfigSchemaV1>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* User configuration for Webex - users provide their Person ID for direct messages.
|
|
31
|
+
*/
|
|
32
|
+
const webexUserConfigSchema = z.object({
|
|
33
|
+
personId: z.string().min(1).describe("Your Webex Person ID"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
type WebexUserConfig = z.infer<typeof webexUserConfigSchema>;
|
|
37
|
+
|
|
38
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
39
|
+
// Instructions
|
|
40
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
41
|
+
|
|
42
|
+
const adminInstructions = `
|
|
43
|
+
## Create a Webex Bot
|
|
44
|
+
|
|
45
|
+
1. Go to [developer.webex.com](https://developer.webex.com/) and sign in
|
|
46
|
+
2. Navigate to **My Webex Apps** → **Create a New App** → **Create a Bot**
|
|
47
|
+
3. Fill in the bot details:
|
|
48
|
+
- **Bot Name**: Your notification bot name (e.g., "Checkstack Alerts")
|
|
49
|
+
- **Bot Username**: A unique username
|
|
50
|
+
- **Icon**: Upload an icon or use default
|
|
51
|
+
- **Description**: Brief description of the bot
|
|
52
|
+
4. Click **Create Bot**
|
|
53
|
+
5. Copy the **Bot Access Token** — this is shown only once!
|
|
54
|
+
|
|
55
|
+
> **Important**: Bot tokens are long-lived (100+ years) but can only be viewed once. Store it securely.
|
|
56
|
+
`.trim();
|
|
57
|
+
|
|
58
|
+
const userInstructions = `
|
|
59
|
+
## Get Your Webex Person ID
|
|
60
|
+
|
|
61
|
+
1. Open your Webex app and start a chat with your organization's notification bot
|
|
62
|
+
2. Send any message to the bot (this creates the 1:1 space)
|
|
63
|
+
3. To find your Person ID, use one of these methods:
|
|
64
|
+
|
|
65
|
+
**Method 1: Via Webex Developer Portal**
|
|
66
|
+
- Go to [developer.webex.com/docs/api/v1/people/get-my-own-details](https://developer.webex.com/docs/api/v1/people/get-my-own-details)
|
|
67
|
+
- Click "Run" — your Person ID is in the response
|
|
68
|
+
|
|
69
|
+
**Method 2: Ask your admin**
|
|
70
|
+
- Your Webex admin can look up your Person ID via the admin console
|
|
71
|
+
|
|
72
|
+
4. Enter your Person ID above and save
|
|
73
|
+
|
|
74
|
+
> **Note**: You must have messaged the bot at least once before notifications can be sent.
|
|
75
|
+
`.trim();
|
|
76
|
+
|
|
77
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
78
|
+
// Webex Strategy Implementation
|
|
79
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
80
|
+
|
|
81
|
+
const WEBEX_API_BASE = "https://webexapis.com/v1";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Webex notification strategy.
|
|
85
|
+
* Sends notifications as direct messages via the Webex Messages API.
|
|
86
|
+
*/
|
|
87
|
+
const webexStrategy: NotificationStrategy<WebexConfig, WebexUserConfig> = {
|
|
88
|
+
id: "webex",
|
|
89
|
+
displayName: "Webex",
|
|
90
|
+
description: "Send notifications via Webex direct messages",
|
|
91
|
+
icon: "MessageSquare",
|
|
92
|
+
|
|
93
|
+
config: new Versioned({
|
|
94
|
+
version: 1,
|
|
95
|
+
schema: webexConfigSchemaV1,
|
|
96
|
+
}),
|
|
97
|
+
|
|
98
|
+
// User-config resolution - users enter their Person ID
|
|
99
|
+
contactResolution: { type: "user-config", field: "personId" },
|
|
100
|
+
|
|
101
|
+
userConfig: new Versioned({
|
|
102
|
+
version: 1,
|
|
103
|
+
schema: webexUserConfigSchema,
|
|
104
|
+
}),
|
|
105
|
+
|
|
106
|
+
adminInstructions,
|
|
107
|
+
userInstructions,
|
|
108
|
+
|
|
109
|
+
async send(
|
|
110
|
+
context: NotificationSendContext<WebexConfig, WebexUserConfig>
|
|
111
|
+
): Promise<NotificationDeliveryResult> {
|
|
112
|
+
const { userConfig, notification, strategyConfig } = context;
|
|
113
|
+
|
|
114
|
+
if (!strategyConfig.botToken) {
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: "Webex bot token not configured",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!userConfig?.personId) {
|
|
122
|
+
return {
|
|
123
|
+
success: false,
|
|
124
|
+
error: "User has not configured their Webex Person ID",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
// Build message with markdown formatting
|
|
130
|
+
const importanceEmoji = {
|
|
131
|
+
info: "ℹ️",
|
|
132
|
+
warning: "⚠️",
|
|
133
|
+
critical: "🚨",
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
let markdown = `${importanceEmoji[notification.importance]} **${
|
|
137
|
+
notification.title
|
|
138
|
+
}**`;
|
|
139
|
+
|
|
140
|
+
if (notification.body) {
|
|
141
|
+
markdown += `\n\n${notification.body}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (notification.action?.url) {
|
|
145
|
+
markdown += `\n\n[${notification.action.label}](${notification.action.url})`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Send message via Webex API
|
|
149
|
+
const response = await fetch(`${WEBEX_API_BASE}/messages`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
Authorization: `Bearer ${strategyConfig.botToken}`,
|
|
153
|
+
"Content-Type": "application/json",
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({
|
|
156
|
+
toPersonId: userConfig.personId,
|
|
157
|
+
markdown,
|
|
158
|
+
}),
|
|
159
|
+
signal: AbortSignal.timeout(10_000),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
const errorText = await response.text();
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `Webex API error (${response.status}): ${errorText.slice(
|
|
167
|
+
0,
|
|
168
|
+
200
|
|
169
|
+
)}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const result = (await response.json()) as { id?: string };
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
success: true,
|
|
177
|
+
externalId: result.id,
|
|
178
|
+
};
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const message =
|
|
181
|
+
error instanceof Error ? error.message : "Unknown Webex API error";
|
|
182
|
+
return {
|
|
183
|
+
success: false,
|
|
184
|
+
error: `Failed to send Webex message: ${message}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
191
|
+
// Plugin Definition
|
|
192
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
193
|
+
|
|
194
|
+
export default createBackendPlugin({
|
|
195
|
+
metadata: pluginMetadata,
|
|
196
|
+
|
|
197
|
+
register(env) {
|
|
198
|
+
// Get the notification strategy extension point
|
|
199
|
+
const extensionPoint = env.getExtensionPoint(
|
|
200
|
+
notificationStrategyExtensionPoint
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Register the Webex strategy with our plugin metadata
|
|
204
|
+
extensionPoint.addStrategy(webexStrategy, pluginMetadata);
|
|
205
|
+
},
|
|
206
|
+
});
|
package/tsconfig.json
ADDED