@checkstack/integration-teams-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/CHANGELOG.md +206 -0
- package/package.json +8 -5
- package/src/automations.test.ts +289 -0
- package/src/automations.ts +213 -0
- package/src/index.ts +27 -6
- package/src/provider.ts +61 -296
- package/tsconfig.json +6 -0
- package/src/provider.test.ts +0 -486
package/src/provider.ts
CHANGED
|
@@ -1,31 +1,43 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { configString, Versioned } from "@checkstack/backend-api";
|
|
3
3
|
import type {
|
|
4
|
-
IntegrationProvider,
|
|
5
|
-
IntegrationDeliveryContext,
|
|
6
|
-
IntegrationDeliveryResult,
|
|
7
|
-
GetConnectionOptionsParams,
|
|
8
4
|
ConnectionOption,
|
|
5
|
+
GetConnectionOptionsParams,
|
|
6
|
+
IntegrationProvider,
|
|
9
7
|
TestConnectionResult,
|
|
10
8
|
} from "@checkstack/integration-backend";
|
|
11
9
|
import { extractErrorMessage } from "@checkstack/common";
|
|
10
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
11
|
+
|
|
12
|
+
// ─── Provider id ─────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** Local provider id (namespaced on registration to `{pluginId}.{id}`). */
|
|
15
|
+
export const TEAMS_PROVIDER_LOCAL_ID = "teams";
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Fully-qualified Teams provider id (`integration-teams.teams`). 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 TEAMS_PROVIDER_QUALIFIED_ID = `${pluginMetadata.pluginId}.${TEAMS_PROVIDER_LOCAL_ID}`;
|
|
25
|
+
|
|
26
|
+
// ─── Resolver names ─────────────────────────────────────────────────────
|
|
16
27
|
|
|
17
|
-
const TEAMS_RESOLVERS = {
|
|
28
|
+
export const TEAMS_RESOLVERS = {
|
|
29
|
+
/**
|
|
30
|
+
* Site-wide Teams connections. Drives the connection picker on the Teams
|
|
31
|
+
* action; the editor bridge resolves it via `listConnections` (no
|
|
32
|
+
* connection is selected yet), not `getConnectionOptions`.
|
|
33
|
+
*/
|
|
34
|
+
CONNECTION_OPTIONS: "connectionOptions",
|
|
18
35
|
TEAM_OPTIONS: "teamOptions",
|
|
19
36
|
CHANNEL_OPTIONS: "channelOptions",
|
|
20
37
|
} as const;
|
|
21
38
|
|
|
22
|
-
//
|
|
23
|
-
// Configuration Schemas
|
|
24
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
39
|
+
// ─── Connection schema ──────────────────────────────────────────────────
|
|
25
40
|
|
|
26
|
-
/**
|
|
27
|
-
* Connection configuration - Azure AD App credentials for Graph API.
|
|
28
|
-
*/
|
|
29
41
|
export const TeamsConnectionSchema = z.object({
|
|
30
42
|
tenantId: configString({}).describe("Azure AD Tenant ID"),
|
|
31
43
|
clientId: configString({}).describe("Azure AD Application (Client) ID"),
|
|
@@ -36,26 +48,7 @@ export const TeamsConnectionSchema = z.object({
|
|
|
36
48
|
|
|
37
49
|
export type TeamsConnectionConfig = z.infer<typeof TeamsConnectionSchema>;
|
|
38
50
|
|
|
39
|
-
|
|
40
|
-
* Subscription configuration - which Teams channel to send events to.
|
|
41
|
-
*/
|
|
42
|
-
export const TeamsSubscriptionSchema = z.object({
|
|
43
|
-
connectionId: configString({ "x-hidden": true }).describe("Teams connection"),
|
|
44
|
-
teamId: configString({
|
|
45
|
-
"x-options-resolver": TEAMS_RESOLVERS.TEAM_OPTIONS,
|
|
46
|
-
"x-depends-on": ["connectionId"],
|
|
47
|
-
}).describe("Target Team"),
|
|
48
|
-
channelId: configString({
|
|
49
|
-
"x-options-resolver": TEAMS_RESOLVERS.CHANNEL_OPTIONS,
|
|
50
|
-
"x-depends-on": ["connectionId", "teamId"],
|
|
51
|
-
}).describe("Target Channel"),
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
export type TeamsSubscriptionConfig = z.infer<typeof TeamsSubscriptionSchema>;
|
|
55
|
-
|
|
56
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
57
|
-
// Graph API Types and Client
|
|
58
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
51
|
+
// ─── Graph API helpers ──────────────────────────────────────────────────
|
|
59
52
|
|
|
60
53
|
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
|
61
54
|
|
|
@@ -69,18 +62,6 @@ interface GraphChannel {
|
|
|
69
62
|
displayName: string;
|
|
70
63
|
}
|
|
71
64
|
|
|
72
|
-
interface GraphTeamsResponse {
|
|
73
|
-
value: GraphTeam[];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
interface GraphChannelsResponse {
|
|
77
|
-
value: GraphChannel[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
interface GraphMessageResponse {
|
|
81
|
-
id: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
65
|
interface TokenResponse {
|
|
85
66
|
access_token: string;
|
|
86
67
|
expires_in: number;
|
|
@@ -88,20 +69,20 @@ interface TokenResponse {
|
|
|
88
69
|
|
|
89
70
|
/**
|
|
90
71
|
* Get an app-only access token using client credentials flow.
|
|
72
|
+
*
|
|
73
|
+
* Exported for `automations.ts` so the post-message action can reuse
|
|
74
|
+
* it without duplicating the OAuth dance.
|
|
91
75
|
*/
|
|
92
|
-
async function getAppToken(
|
|
76
|
+
export async function getAppToken(
|
|
93
77
|
config: TeamsConnectionConfig,
|
|
94
78
|
): Promise<
|
|
95
79
|
{ success: true; token: string } | { success: false; error: string }
|
|
96
80
|
> {
|
|
97
81
|
try {
|
|
98
82
|
const tokenUrl = `https://login.microsoftonline.com/${config.tenantId}/oauth2/v2.0/token`;
|
|
99
|
-
|
|
100
83
|
const response = await fetch(tokenUrl, {
|
|
101
84
|
method: "POST",
|
|
102
|
-
headers: {
|
|
103
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
104
|
-
},
|
|
85
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
105
86
|
body: new URLSearchParams({
|
|
106
87
|
client_id: config.clientId,
|
|
107
88
|
client_secret: config.clientSecret,
|
|
@@ -110,23 +91,17 @@ async function getAppToken(
|
|
|
110
91
|
}),
|
|
111
92
|
signal: AbortSignal.timeout(10_000),
|
|
112
93
|
});
|
|
113
|
-
|
|
114
94
|
if (!response.ok) {
|
|
115
95
|
const errorText = await response.text();
|
|
116
96
|
return {
|
|
117
97
|
success: false,
|
|
118
|
-
error: `Token request failed (${response.status}): ${errorText.slice(
|
|
119
|
-
0,
|
|
120
|
-
200,
|
|
121
|
-
)}`,
|
|
98
|
+
error: `Token request failed (${response.status}): ${errorText.slice(0, 200)}`,
|
|
122
99
|
};
|
|
123
100
|
}
|
|
124
|
-
|
|
125
101
|
const data = (await response.json()) as TokenResponse;
|
|
126
102
|
return { success: true, token: data.access_token };
|
|
127
103
|
} catch (error) {
|
|
128
|
-
|
|
129
|
-
return { success: false, error: message };
|
|
104
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
130
105
|
}
|
|
131
106
|
}
|
|
132
107
|
|
|
@@ -137,21 +112,16 @@ async function fetchTeams(
|
|
|
137
112
|
> {
|
|
138
113
|
try {
|
|
139
114
|
const response = await fetch(`${GRAPH_API_BASE}/teams`, {
|
|
140
|
-
headers: {
|
|
141
|
-
Authorization: `Bearer ${token}`,
|
|
142
|
-
},
|
|
115
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
143
116
|
signal: AbortSignal.timeout(10_000),
|
|
144
117
|
});
|
|
145
|
-
|
|
146
118
|
if (!response.ok) {
|
|
147
119
|
return { success: false, error: `Graph API error: ${response.status}` };
|
|
148
120
|
}
|
|
149
|
-
|
|
150
|
-
const data = (await response.json()) as GraphTeamsResponse;
|
|
121
|
+
const data = (await response.json()) as { value: GraphTeam[] };
|
|
151
122
|
return { success: true, teams: data.value ?? [] };
|
|
152
123
|
} catch (error) {
|
|
153
|
-
|
|
154
|
-
return { success: false, error: message };
|
|
124
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
155
125
|
}
|
|
156
126
|
}
|
|
157
127
|
|
|
@@ -159,99 +129,31 @@ async function fetchChannels(
|
|
|
159
129
|
token: string,
|
|
160
130
|
teamId: string,
|
|
161
131
|
): Promise<
|
|
162
|
-
|
|
163
|
-
| { success: false; error: string }
|
|
132
|
+
{ success: true; channels: GraphChannel[] } | { success: false; error: string }
|
|
164
133
|
> {
|
|
165
134
|
try {
|
|
166
135
|
const response = await fetch(`${GRAPH_API_BASE}/teams/${teamId}/channels`, {
|
|
167
|
-
headers: {
|
|
168
|
-
Authorization: `Bearer ${token}`,
|
|
169
|
-
},
|
|
136
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
170
137
|
signal: AbortSignal.timeout(10_000),
|
|
171
138
|
});
|
|
172
|
-
|
|
173
139
|
if (!response.ok) {
|
|
174
140
|
return { success: false, error: `Graph API error: ${response.status}` };
|
|
175
141
|
}
|
|
176
|
-
|
|
177
|
-
const data = (await response.json()) as GraphChannelsResponse;
|
|
142
|
+
const data = (await response.json()) as { value: GraphChannel[] };
|
|
178
143
|
return { success: true, channels: data.value ?? [] };
|
|
179
144
|
} catch (error) {
|
|
180
|
-
|
|
181
|
-
return { success: false, error: message };
|
|
145
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
182
146
|
}
|
|
183
147
|
}
|
|
184
148
|
|
|
185
|
-
//
|
|
186
|
-
// Adaptive Card Builder
|
|
187
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
188
|
-
|
|
189
|
-
interface AdaptiveCardOptions {
|
|
190
|
-
eventId: string;
|
|
191
|
-
payload: Record<string, unknown>;
|
|
192
|
-
subscriptionName: string;
|
|
193
|
-
timestamp: string;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export function buildAdaptiveCard(options: AdaptiveCardOptions): object {
|
|
197
|
-
const { eventId, payload, subscriptionName, timestamp } = options;
|
|
198
|
-
|
|
199
|
-
return {
|
|
200
|
-
type: "AdaptiveCard",
|
|
201
|
-
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
202
|
-
version: "1.4",
|
|
203
|
-
body: [
|
|
204
|
-
{
|
|
205
|
-
type: "TextBlock",
|
|
206
|
-
text: `📢 Integration Event`,
|
|
207
|
-
weight: "bolder",
|
|
208
|
-
size: "large",
|
|
209
|
-
wrap: true,
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
type: "FactSet",
|
|
213
|
-
facts: [
|
|
214
|
-
{ title: "Event", value: eventId },
|
|
215
|
-
{ title: "Subscription", value: subscriptionName },
|
|
216
|
-
{ title: "Time", value: new Date(timestamp).toLocaleString() },
|
|
217
|
-
],
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
type: "TextBlock",
|
|
221
|
-
text: "**Payload:**",
|
|
222
|
-
weight: "bolder",
|
|
223
|
-
spacing: "medium",
|
|
224
|
-
},
|
|
225
|
-
{
|
|
226
|
-
type: "TextBlock",
|
|
227
|
-
|
|
228
|
-
text: "```\n" + JSON.stringify(payload, null, 2) + "\n```",
|
|
229
|
-
wrap: true,
|
|
230
|
-
fontType: "monospace",
|
|
231
|
-
size: "small",
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
238
|
-
// Provider Implementation
|
|
239
|
-
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
149
|
+
// ─── Provider definition (connection-only) ──────────────────────────────
|
|
240
150
|
|
|
241
|
-
export const teamsProvider: IntegrationProvider<
|
|
242
|
-
|
|
243
|
-
TeamsConnectionConfig
|
|
244
|
-
> = {
|
|
245
|
-
id: "teams",
|
|
151
|
+
export const teamsProvider: IntegrationProvider<TeamsConnectionConfig> = {
|
|
152
|
+
id: TEAMS_PROVIDER_LOCAL_ID,
|
|
246
153
|
displayName: "Microsoft Teams",
|
|
247
|
-
description: "Send
|
|
154
|
+
description: "Send automation messages to Microsoft Teams channels",
|
|
248
155
|
icon: "MessageSquareMore",
|
|
249
156
|
|
|
250
|
-
config: new Versioned({
|
|
251
|
-
version: 1,
|
|
252
|
-
schema: TeamsSubscriptionSchema,
|
|
253
|
-
}),
|
|
254
|
-
|
|
255
157
|
connectionSchema: new Versioned({
|
|
256
158
|
version: 1,
|
|
257
159
|
schema: TeamsConnectionSchema,
|
|
@@ -262,59 +164,36 @@ export const teamsProvider: IntegrationProvider<
|
|
|
262
164
|
## Register an Azure AD Application
|
|
263
165
|
|
|
264
166
|
1. Go to [Azure Portal](https://portal.azure.com/) → **Microsoft Entra ID**
|
|
265
|
-
2.
|
|
266
|
-
3. Fill in details and register
|
|
167
|
+
2. **App registrations** → **New registration**, register the app
|
|
267
168
|
|
|
268
169
|
## Configure API Permissions
|
|
269
170
|
|
|
270
|
-
1.
|
|
271
|
-
2.
|
|
272
|
-
3. Add
|
|
273
|
-
|
|
274
|
-
- \`Channel.ReadBasic.All\` (to list channels)
|
|
275
|
-
- \`ChannelMessage.Send\` (to send messages)
|
|
276
|
-
4. Click **Grant admin consent**
|
|
171
|
+
1. **API permissions** → **Add a permission** → **Microsoft Graph**
|
|
172
|
+
2. **Application permissions** (not Delegated)
|
|
173
|
+
3. Add: \`Team.ReadBasic.All\`, \`Channel.ReadBasic.All\`, \`ChannelMessage.Send\`
|
|
174
|
+
4. **Grant admin consent**
|
|
277
175
|
|
|
278
176
|
## Create Client Secret
|
|
279
177
|
|
|
280
|
-
1.
|
|
178
|
+
1. **Certificates & secrets** → **New client secret**
|
|
281
179
|
2. Copy the secret value immediately
|
|
282
|
-
|
|
283
|
-
## Add App to Teams
|
|
284
|
-
|
|
285
|
-
For the app to send messages, it must be installed in the target Team:
|
|
286
|
-
1. Create a Teams app manifest or use Graph API to install
|
|
287
|
-
2. Alternatively, ensure the app has \`ChannelMessage.Send\` consent
|
|
288
180
|
`.trim(),
|
|
289
181
|
},
|
|
290
182
|
|
|
291
183
|
async getConnectionOptions(
|
|
292
184
|
params: GetConnectionOptionsParams,
|
|
293
185
|
): Promise<ConnectionOption[]> {
|
|
294
|
-
const {
|
|
295
|
-
|
|
296
|
-
connectionId,
|
|
297
|
-
context,
|
|
298
|
-
getConnectionWithCredentials,
|
|
299
|
-
} = params;
|
|
300
|
-
|
|
186
|
+
const { resolverName, connectionId, context, getConnectionWithCredentials } =
|
|
187
|
+
params;
|
|
301
188
|
const connection = await getConnectionWithCredentials(connectionId);
|
|
302
|
-
if (!connection)
|
|
303
|
-
return [];
|
|
304
|
-
}
|
|
305
|
-
|
|
189
|
+
if (!connection) return [];
|
|
306
190
|
const config = connection.config as TeamsConnectionConfig;
|
|
307
|
-
|
|
308
191
|
const tokenResult = await getAppToken(config);
|
|
309
|
-
if (!tokenResult.success)
|
|
310
|
-
return [];
|
|
311
|
-
}
|
|
192
|
+
if (!tokenResult.success) return [];
|
|
312
193
|
|
|
313
194
|
if (resolverName === TEAMS_RESOLVERS.TEAM_OPTIONS) {
|
|
314
195
|
const result = await fetchTeams(tokenResult.token);
|
|
315
|
-
if (!result.success)
|
|
316
|
-
return [];
|
|
317
|
-
}
|
|
196
|
+
if (!result.success) return [];
|
|
318
197
|
return result.teams.map((team) => ({
|
|
319
198
|
value: team.id,
|
|
320
199
|
label: team.displayName,
|
|
@@ -322,37 +201,28 @@ For the app to send messages, it must be installed in the target Team:
|
|
|
322
201
|
}
|
|
323
202
|
|
|
324
203
|
if (resolverName === TEAMS_RESOLVERS.CHANNEL_OPTIONS) {
|
|
325
|
-
const teamId =
|
|
326
|
-
if (!teamId)
|
|
327
|
-
return [];
|
|
328
|
-
}
|
|
329
|
-
|
|
204
|
+
const teamId = context?.teamId as string | undefined;
|
|
205
|
+
if (!teamId) return [];
|
|
330
206
|
const result = await fetchChannels(tokenResult.token, teamId);
|
|
331
|
-
if (!result.success)
|
|
332
|
-
return [];
|
|
333
|
-
}
|
|
207
|
+
if (!result.success) return [];
|
|
334
208
|
return result.channels.map((channel) => ({
|
|
335
209
|
value: channel.id,
|
|
336
210
|
label: channel.displayName,
|
|
337
211
|
}));
|
|
338
212
|
}
|
|
339
|
-
|
|
340
213
|
return [];
|
|
341
214
|
},
|
|
342
215
|
|
|
343
|
-
async testConnection(config
|
|
216
|
+
async testConnection(config): Promise<TestConnectionResult> {
|
|
344
217
|
try {
|
|
345
218
|
const parsedConfig = TeamsConnectionSchema.parse(config);
|
|
346
219
|
const tokenResult = await getAppToken(parsedConfig);
|
|
347
|
-
|
|
348
220
|
if (!tokenResult.success) {
|
|
349
221
|
return {
|
|
350
222
|
success: false,
|
|
351
223
|
message: `Authentication failed: ${tokenResult.error}`,
|
|
352
224
|
};
|
|
353
225
|
}
|
|
354
|
-
|
|
355
|
-
// Verify Graph API access by listing teams
|
|
356
226
|
const teamsResult = await fetchTeams(tokenResult.token);
|
|
357
227
|
if (!teamsResult.success) {
|
|
358
228
|
return {
|
|
@@ -360,119 +230,14 @@ For the app to send messages, it must be installed in the target Team:
|
|
|
360
230
|
message: `API access failed: ${teamsResult.error}`,
|
|
361
231
|
};
|
|
362
232
|
}
|
|
363
|
-
|
|
364
233
|
return {
|
|
365
234
|
success: true,
|
|
366
235
|
message: `Connected successfully. Found ${teamsResult.teams.length} team(s).`,
|
|
367
236
|
};
|
|
368
237
|
} catch (error) {
|
|
369
|
-
const message = extractErrorMessage(error, "Invalid configuration");
|
|
370
|
-
return {
|
|
371
|
-
success: false,
|
|
372
|
-
message: `Validation failed: ${message}`,
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
},
|
|
376
|
-
|
|
377
|
-
async deliver(
|
|
378
|
-
context: IntegrationDeliveryContext<TeamsSubscriptionConfig>,
|
|
379
|
-
): Promise<IntegrationDeliveryResult> {
|
|
380
|
-
const { event, subscription, providerConfig, logger } = context;
|
|
381
|
-
|
|
382
|
-
const config = TeamsSubscriptionSchema.parse(providerConfig);
|
|
383
|
-
|
|
384
|
-
if (!context.getConnectionWithCredentials) {
|
|
385
|
-
return {
|
|
386
|
-
success: false,
|
|
387
|
-
error: "Connection credentials not available",
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const connection = await context.getConnectionWithCredentials(
|
|
392
|
-
config.connectionId,
|
|
393
|
-
);
|
|
394
|
-
|
|
395
|
-
if (!connection) {
|
|
396
|
-
return {
|
|
397
|
-
success: false,
|
|
398
|
-
error: `Connection not found: ${config.connectionId}`,
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const connectionConfig = connection.config as TeamsConnectionConfig;
|
|
403
|
-
|
|
404
|
-
const tokenResult = await getAppToken(connectionConfig);
|
|
405
|
-
if (!tokenResult.success) {
|
|
406
|
-
logger.error("Failed to get Graph API token", {
|
|
407
|
-
error: tokenResult.error,
|
|
408
|
-
});
|
|
409
|
-
return {
|
|
410
|
-
success: false,
|
|
411
|
-
error: `Authentication failed: ${tokenResult.error}`,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const adaptiveCard = buildAdaptiveCard({
|
|
416
|
-
eventId: event.eventId,
|
|
417
|
-
payload: event.payload as Record<string, unknown>,
|
|
418
|
-
subscriptionName: subscription.name,
|
|
419
|
-
timestamp: event.timestamp,
|
|
420
|
-
});
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
const response = await fetch(
|
|
424
|
-
`${GRAPH_API_BASE}/teams/${config.teamId}/channels/${config.channelId}/messages`,
|
|
425
|
-
{
|
|
426
|
-
method: "POST",
|
|
427
|
-
headers: {
|
|
428
|
-
Authorization: `Bearer ${tokenResult.token}`,
|
|
429
|
-
"Content-Type": "application/json",
|
|
430
|
-
},
|
|
431
|
-
body: JSON.stringify({
|
|
432
|
-
body: {
|
|
433
|
-
contentType: "html",
|
|
434
|
-
content: `<attachment id="adaptiveCard"></attachment>`,
|
|
435
|
-
},
|
|
436
|
-
attachments: [
|
|
437
|
-
{
|
|
438
|
-
id: "adaptiveCard",
|
|
439
|
-
contentType: "application/vnd.microsoft.card.adaptive",
|
|
440
|
-
content: JSON.stringify(adaptiveCard),
|
|
441
|
-
},
|
|
442
|
-
],
|
|
443
|
-
}),
|
|
444
|
-
signal: AbortSignal.timeout(10_000),
|
|
445
|
-
},
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
if (!response.ok) {
|
|
449
|
-
const errorText = await response.text();
|
|
450
|
-
logger.error("Failed to send Teams message", {
|
|
451
|
-
status: response.status,
|
|
452
|
-
error: errorText.slice(0, 200),
|
|
453
|
-
});
|
|
454
|
-
return {
|
|
455
|
-
success: false,
|
|
456
|
-
error: `Graph API error (${response.status}): ${errorText.slice(
|
|
457
|
-
0,
|
|
458
|
-
100,
|
|
459
|
-
)}`,
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
const messageData = (await response.json()) as GraphMessageResponse;
|
|
464
|
-
|
|
465
|
-
logger.info("Teams message sent", { messageId: messageData.id });
|
|
466
|
-
return {
|
|
467
|
-
success: true,
|
|
468
|
-
externalId: messageData.id,
|
|
469
|
-
};
|
|
470
|
-
} catch (error) {
|
|
471
|
-
const message = extractErrorMessage(error, "Unknown Graph API error");
|
|
472
|
-
logger.error("Teams delivery error", { error: message });
|
|
473
238
|
return {
|
|
474
239
|
success: false,
|
|
475
|
-
|
|
240
|
+
message: `Validation failed: ${extractErrorMessage(error, "Invalid configuration")}`,
|
|
476
241
|
};
|
|
477
242
|
}
|
|
478
243
|
},
|
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
|
}
|