@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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teams action — post a message to a Microsoft Teams channel.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the legacy `teamsProvider.deliver`. The operator now writes
|
|
5
|
+
* the message body (with markdown/template support handled at the
|
|
6
|
+
* dispatch-engine level); this action just wraps it in a minimal
|
|
7
|
+
* Adaptive Card and posts it via the Graph API.
|
|
8
|
+
*/
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import {
|
|
11
|
+
configString,
|
|
12
|
+
Versioned,
|
|
13
|
+
type Logger,
|
|
14
|
+
} from "@checkstack/backend-api";
|
|
15
|
+
import type { ActionDefinition } from "@checkstack/automation-backend";
|
|
16
|
+
import { connectionStoreRef } from "@checkstack/integration-backend";
|
|
17
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
getAppToken,
|
|
21
|
+
TEAMS_RESOLVERS,
|
|
22
|
+
TEAMS_PROVIDER_QUALIFIED_ID,
|
|
23
|
+
type TeamsConnectionConfig,
|
|
24
|
+
} from "./provider";
|
|
25
|
+
|
|
26
|
+
const GRAPH_API_BASE = "https://graph.microsoft.com/v1.0";
|
|
27
|
+
|
|
28
|
+
const teamsMessageDataSchema = z.object({
|
|
29
|
+
messageId: z.string(),
|
|
30
|
+
teamId: z.string(),
|
|
31
|
+
channelId: z.string(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const teamsMessageArtifactType = {
|
|
35
|
+
id: "message",
|
|
36
|
+
displayName: "Teams Message",
|
|
37
|
+
description: "A message posted to a Teams channel",
|
|
38
|
+
schema: teamsMessageDataSchema,
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
const teamsPostMessageConfigSchema = z.object({
|
|
42
|
+
connectionId: configString({
|
|
43
|
+
"x-options-resolver": TEAMS_RESOLVERS.CONNECTION_OPTIONS,
|
|
44
|
+
}).describe("Teams connection"),
|
|
45
|
+
teamId: configString({
|
|
46
|
+
"x-options-resolver": TEAMS_RESOLVERS.TEAM_OPTIONS,
|
|
47
|
+
"x-depends-on": ["connectionId"],
|
|
48
|
+
}).describe("Target Team"),
|
|
49
|
+
channelId: configString({
|
|
50
|
+
"x-options-resolver": TEAMS_RESOLVERS.CHANNEL_OPTIONS,
|
|
51
|
+
"x-depends-on": ["connectionId", "teamId"],
|
|
52
|
+
}).describe("Target Channel"),
|
|
53
|
+
title: configString({ "x-editor-types": ["raw"] })
|
|
54
|
+
.optional()
|
|
55
|
+
.describe("Card title (optional)"),
|
|
56
|
+
body: configString({ "x-editor-types": ["raw"] })
|
|
57
|
+
.min(1)
|
|
58
|
+
.describe("Message body — supports markdown"),
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
type TeamsPostMessageConfig = z.infer<typeof teamsPostMessageConfigSchema>;
|
|
62
|
+
|
|
63
|
+
function buildAdaptiveCard({
|
|
64
|
+
title,
|
|
65
|
+
body,
|
|
66
|
+
}: {
|
|
67
|
+
title?: string;
|
|
68
|
+
body: string;
|
|
69
|
+
}): Record<string, unknown> {
|
|
70
|
+
const elements: Array<Record<string, unknown>> = [];
|
|
71
|
+
if (title) {
|
|
72
|
+
elements.push({
|
|
73
|
+
type: "TextBlock",
|
|
74
|
+
text: title,
|
|
75
|
+
weight: "bolder",
|
|
76
|
+
size: "large",
|
|
77
|
+
wrap: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
elements.push({
|
|
81
|
+
type: "TextBlock",
|
|
82
|
+
text: body,
|
|
83
|
+
wrap: true,
|
|
84
|
+
});
|
|
85
|
+
return {
|
|
86
|
+
type: "AdaptiveCard",
|
|
87
|
+
$schema: "http://adaptivecards.io/schemas/adaptive-card.json",
|
|
88
|
+
version: "1.4",
|
|
89
|
+
body: elements,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function postTeamsMessage(params: {
|
|
94
|
+
token: string;
|
|
95
|
+
teamId: string;
|
|
96
|
+
channelId: string;
|
|
97
|
+
card: Record<string, unknown>;
|
|
98
|
+
logger: Logger;
|
|
99
|
+
}): Promise<
|
|
100
|
+
{ success: true; messageId: string } | { success: false; error: string }
|
|
101
|
+
> {
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(
|
|
104
|
+
`${GRAPH_API_BASE}/teams/${params.teamId}/channels/${params.channelId}/messages`,
|
|
105
|
+
{
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: {
|
|
108
|
+
Authorization: `Bearer ${params.token}`,
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
},
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
body: {
|
|
113
|
+
contentType: "html",
|
|
114
|
+
content: `<attachment id="adaptiveCard"></attachment>`,
|
|
115
|
+
},
|
|
116
|
+
attachments: [
|
|
117
|
+
{
|
|
118
|
+
id: "adaptiveCard",
|
|
119
|
+
contentType: "application/vnd.microsoft.card.adaptive",
|
|
120
|
+
content: JSON.stringify(params.card),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
}),
|
|
124
|
+
signal: AbortSignal.timeout(10_000),
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
const errorText = await response.text();
|
|
129
|
+
params.logger.error("Teams Graph API error", {
|
|
130
|
+
status: response.status,
|
|
131
|
+
error: errorText.slice(0, 200),
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
success: false,
|
|
135
|
+
error: `Graph API error (${response.status}): ${errorText.slice(0, 100)}`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const data = (await response.json()) as { id: string };
|
|
139
|
+
return { success: true, messageId: data.id };
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { success: false, error: extractErrorMessage(error, "Unknown error") };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createTeamsActions(): ActionDefinition<unknown, unknown>[] {
|
|
146
|
+
const postMessage: ActionDefinition<
|
|
147
|
+
TeamsPostMessageConfig,
|
|
148
|
+
z.infer<typeof teamsMessageDataSchema>
|
|
149
|
+
> = {
|
|
150
|
+
id: "post_message",
|
|
151
|
+
displayName: "Post Teams Message",
|
|
152
|
+
description: "Send a message to a Teams channel",
|
|
153
|
+
category: "Microsoft Teams",
|
|
154
|
+
icon: "MessageSquareMore",
|
|
155
|
+
connectionProviderId: TEAMS_PROVIDER_QUALIFIED_ID,
|
|
156
|
+
config: new Versioned({
|
|
157
|
+
version: 1,
|
|
158
|
+
schema: teamsPostMessageConfigSchema,
|
|
159
|
+
}),
|
|
160
|
+
produces: "message",
|
|
161
|
+
// Fetch the connection store lazily here rather than baking it in
|
|
162
|
+
// at registration time: integration-backend registers
|
|
163
|
+
// `connectionStoreRef` inside its own `init()`, so the topological
|
|
164
|
+
// sort can't statically order this plugin's init after that. By
|
|
165
|
+
// execute time every plugin has finished init + afterPluginsReady,
|
|
166
|
+
// so `getService(connectionStoreRef)` is always resolvable.
|
|
167
|
+
execute: async ({ config, logger, getService }) => {
|
|
168
|
+
const connectionStore = await getService(connectionStoreRef);
|
|
169
|
+
const connection = await connectionStore.getConnectionWithCredentials(
|
|
170
|
+
config.connectionId,
|
|
171
|
+
);
|
|
172
|
+
if (!connection) {
|
|
173
|
+
return {
|
|
174
|
+
success: false,
|
|
175
|
+
error: `Teams connection not found: ${config.connectionId}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
const connConfig = connection.config as TeamsConnectionConfig;
|
|
179
|
+
const tokenResult = await getAppToken(connConfig);
|
|
180
|
+
if (!tokenResult.success) {
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: `Authentication failed: ${tokenResult.error}`,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const card = buildAdaptiveCard({
|
|
187
|
+
title: config.title,
|
|
188
|
+
body: config.body,
|
|
189
|
+
});
|
|
190
|
+
const result = await postTeamsMessage({
|
|
191
|
+
token: tokenResult.token,
|
|
192
|
+
teamId: config.teamId,
|
|
193
|
+
channelId: config.channelId,
|
|
194
|
+
card,
|
|
195
|
+
logger,
|
|
196
|
+
});
|
|
197
|
+
if (!result.success) {
|
|
198
|
+
return { success: false, error: result.error };
|
|
199
|
+
}
|
|
200
|
+
logger.info(`Posted Teams message ${result.messageId}`);
|
|
201
|
+
return {
|
|
202
|
+
success: true,
|
|
203
|
+
externalId: result.messageId,
|
|
204
|
+
artifact: {
|
|
205
|
+
messageId: result.messageId,
|
|
206
|
+
teamId: config.teamId,
|
|
207
|
+
channelId: config.channelId,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
return [postMessage as ActionDefinition<unknown, unknown>];
|
|
213
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import { createBackendPlugin } from "@checkstack/backend-api";
|
|
1
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
2
2
|
import { providerExtensionPoint } from "@checkstack/integration-backend";
|
|
3
|
+
import {
|
|
4
|
+
automationActionExtensionPoint,
|
|
5
|
+
automationArtifactTypeExtensionPoint,
|
|
6
|
+
} from "@checkstack/automation-backend";
|
|
3
7
|
import { pluginMetadata } from "./plugin-metadata";
|
|
4
8
|
import { teamsProvider } from "./provider";
|
|
9
|
+
import {
|
|
10
|
+
createTeamsActions,
|
|
11
|
+
teamsMessageArtifactType,
|
|
12
|
+
} from "./automations";
|
|
5
13
|
|
|
6
14
|
export default createBackendPlugin({
|
|
7
15
|
metadata: pluginMetadata,
|
|
8
|
-
|
|
9
16
|
register(env) {
|
|
10
|
-
|
|
11
|
-
|
|
17
|
+
env
|
|
18
|
+
.getExtensionPoint(providerExtensionPoint)
|
|
19
|
+
.addProvider(teamsProvider, pluginMetadata);
|
|
20
|
+
env
|
|
21
|
+
.getExtensionPoint(automationArtifactTypeExtensionPoint)
|
|
22
|
+
.registerArtifactType(teamsMessageArtifactType, pluginMetadata);
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
24
|
+
env.registerInit({
|
|
25
|
+
deps: {
|
|
26
|
+
logger: coreServices.logger,
|
|
27
|
+
},
|
|
28
|
+
init: async ({ logger }) => {
|
|
29
|
+
const actions = env.getExtensionPoint(automationActionExtensionPoint);
|
|
30
|
+
for (const action of createTeamsActions()) {
|
|
31
|
+
actions.registerAction(action, pluginMetadata);
|
|
32
|
+
}
|
|
33
|
+
logger.debug("✅ Teams automation actions registered");
|
|
34
|
+
},
|
|
35
|
+
});
|
|
15
36
|
},
|
|
16
37
|
});
|