@checkstack/integration-teams-backend 0.0.35 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/provider.ts CHANGED
@@ -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
- // Resolver Names
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
- const message = extractErrorMessage(error, "Unknown error");
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
- const message = extractErrorMessage(error, "Unknown error");
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
- | { success: true; channels: GraphChannel[] }
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
- const message = extractErrorMessage(error, "Unknown error");
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
- TeamsSubscriptionConfig,
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 integration events to Microsoft Teams channels",
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. Navigate to **App registrations** → **New registration**
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. Go to **API permissions** → **Add a permission** → **Microsoft Graph**
271
- 2. Select **Application permissions** (not Delegated)
272
- 3. Add these permissions:
273
- - \`Team.ReadBasic.All\` (to list teams)
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. Go to **Certificates & secrets** → **New client secret**
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
- resolverName,
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 = (context as Partial<TeamsSubscriptionConfig>)?.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: unknown): Promise<TestConnectionResult> {
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
- error: `Failed to send Teams message: ${message}`,
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
  }