@checkstack/notification-slack-backend 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 +7 -0
- package/package.json +25 -0
- package/src/index.test.ts +189 -0
- package/src/index.ts +250 -0
- package/src/plugin-metadata.ts +5 -0
- package/tsconfig.json +3 -0
package/CHANGELOG.md
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@checkstack/notification-slack-backend",
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
"test": "bun test"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@checkstack/backend-api": "workspace:*",
|
|
17
|
+
"@checkstack/common": "workspace:*",
|
|
18
|
+
"@checkstack/notification-backend": "workspace:*",
|
|
19
|
+
"zod": "^4.2.1"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@checkstack/tsconfig": "workspace:*",
|
|
23
|
+
"typescript": "^5.7.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { describe, it, expect, spyOn } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
slackConfigSchemaV1,
|
|
4
|
+
slackUserConfigSchema,
|
|
5
|
+
buildSlackPayload,
|
|
6
|
+
} from "./index";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Unit tests for the Slack Notification Strategy.
|
|
10
|
+
*
|
|
11
|
+
* Tests cover:
|
|
12
|
+
* - Config schema validation
|
|
13
|
+
* - Block Kit payload building
|
|
14
|
+
* - Webhook API interaction
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
describe("Slack Notification Strategy", () => {
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// Config Schema Validation
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("config schema", () => {
|
|
23
|
+
it("accepts empty admin config", () => {
|
|
24
|
+
const result = slackConfigSchemaV1.parse({});
|
|
25
|
+
expect(result).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("validates user config - requires webhookUrl", () => {
|
|
29
|
+
expect(() => {
|
|
30
|
+
slackUserConfigSchema.parse({});
|
|
31
|
+
}).toThrow();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("validates user config - requires valid URL", () => {
|
|
35
|
+
expect(() => {
|
|
36
|
+
slackUserConfigSchema.parse({ webhookUrl: "not-a-url" });
|
|
37
|
+
}).toThrow();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts valid user config", () => {
|
|
41
|
+
const result = slackUserConfigSchema.parse({
|
|
42
|
+
webhookUrl: "https://hooks.slack.com/services/T00/B00/XXX",
|
|
43
|
+
});
|
|
44
|
+
expect(result.webhookUrl).toBe(
|
|
45
|
+
"https://hooks.slack.com/services/T00/B00/XXX",
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Block Kit Payload Building
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
describe("payload builder", () => {
|
|
55
|
+
it("builds payload with title only", () => {
|
|
56
|
+
const payload = buildSlackPayload({
|
|
57
|
+
title: "Test Alert",
|
|
58
|
+
importance: "info",
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(payload.text).toContain("Test Alert");
|
|
62
|
+
expect(payload.text).toContain("ℹ️");
|
|
63
|
+
expect(payload.blocks).toHaveLength(1);
|
|
64
|
+
expect(payload.blocks[0].type).toBe("section");
|
|
65
|
+
expect(payload.attachments).toHaveLength(1);
|
|
66
|
+
expect(payload.attachments![0].color).toBe("#3b82f6");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("builds payload with title and body", () => {
|
|
70
|
+
const payload = buildSlackPayload({
|
|
71
|
+
title: "System Alert",
|
|
72
|
+
body: "The system has recovered.",
|
|
73
|
+
importance: "warning",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(payload.text).toContain("⚠️");
|
|
77
|
+
expect(payload.blocks).toHaveLength(2);
|
|
78
|
+
expect(payload.attachments![0].color).toBe("#f59e0b");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("builds payload with action button", () => {
|
|
82
|
+
const payload = buildSlackPayload({
|
|
83
|
+
title: "Incident Created",
|
|
84
|
+
body: "A new incident requires attention.",
|
|
85
|
+
importance: "critical",
|
|
86
|
+
action: {
|
|
87
|
+
label: "View Incident",
|
|
88
|
+
url: "https://example.com/incident/123",
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(payload.text).toContain("🚨");
|
|
93
|
+
expect(payload.blocks).toHaveLength(3); // header + body + actions
|
|
94
|
+
|
|
95
|
+
const actionsBlock = payload.blocks[2];
|
|
96
|
+
expect(actionsBlock.type).toBe("actions");
|
|
97
|
+
|
|
98
|
+
const elements = actionsBlock.elements as Array<Record<string, unknown>>;
|
|
99
|
+
expect(elements).toHaveLength(1);
|
|
100
|
+
expect(elements[0].type).toBe("button");
|
|
101
|
+
expect(elements[0].url).toBe("https://example.com/incident/123");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("uses correct colors for importance levels", () => {
|
|
105
|
+
const infoPayload = buildSlackPayload({
|
|
106
|
+
title: "Info",
|
|
107
|
+
importance: "info",
|
|
108
|
+
});
|
|
109
|
+
const warningPayload = buildSlackPayload({
|
|
110
|
+
title: "Warning",
|
|
111
|
+
importance: "warning",
|
|
112
|
+
});
|
|
113
|
+
const criticalPayload = buildSlackPayload({
|
|
114
|
+
title: "Critical",
|
|
115
|
+
importance: "critical",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(infoPayload.attachments![0].color).toBe("#3b82f6");
|
|
119
|
+
expect(warningPayload.attachments![0].color).toBe("#f59e0b");
|
|
120
|
+
expect(criticalPayload.attachments![0].color).toBe("#ef4444");
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
125
|
+
// Webhook API Interaction
|
|
126
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
describe("webhook API interaction", () => {
|
|
129
|
+
it("sends payload to webhook URL", async () => {
|
|
130
|
+
let capturedBody: string | undefined;
|
|
131
|
+
let capturedUrl: string | undefined;
|
|
132
|
+
|
|
133
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation((async (
|
|
134
|
+
url: RequestInfo | URL,
|
|
135
|
+
options?: RequestInit,
|
|
136
|
+
) => {
|
|
137
|
+
capturedUrl = url.toString();
|
|
138
|
+
capturedBody = options?.body as string;
|
|
139
|
+
return new Response("ok", { status: 200 });
|
|
140
|
+
}) as unknown as typeof fetch);
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const webhookUrl = "https://hooks.slack.com/services/T00/B00/XXX";
|
|
144
|
+
const payload = buildSlackPayload({
|
|
145
|
+
title: "Test",
|
|
146
|
+
importance: "info",
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
await fetch(webhookUrl, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "Content-Type": "application/json" },
|
|
152
|
+
body: JSON.stringify(payload),
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(capturedUrl).toBe(webhookUrl);
|
|
156
|
+
|
|
157
|
+
const parsedBody = JSON.parse(capturedBody!);
|
|
158
|
+
expect(parsedBody.blocks).toBeDefined();
|
|
159
|
+
expect(parsedBody.text).toContain("Test");
|
|
160
|
+
} finally {
|
|
161
|
+
mockFetch.mockRestore();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("handles API errors gracefully", async () => {
|
|
166
|
+
const mockFetch = spyOn(globalThis, "fetch").mockImplementation(
|
|
167
|
+
(async () => {
|
|
168
|
+
return new Response("invalid_payload", { status: 400 });
|
|
169
|
+
}) as unknown as typeof fetch,
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(
|
|
174
|
+
"https://hooks.slack.com/services/invalid",
|
|
175
|
+
{
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json" },
|
|
178
|
+
body: JSON.stringify({ text: "test" }),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(response.ok).toBe(false);
|
|
183
|
+
expect(response.status).toBe(400);
|
|
184
|
+
} finally {
|
|
185
|
+
mockFetch.mockRestore();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
createBackendPlugin,
|
|
4
|
+
configString,
|
|
5
|
+
Versioned,
|
|
6
|
+
type NotificationStrategy,
|
|
7
|
+
type NotificationSendContext,
|
|
8
|
+
type NotificationDeliveryResult,
|
|
9
|
+
markdownToSlackMrkdwn,
|
|
10
|
+
} from "@checkstack/backend-api";
|
|
11
|
+
import { notificationStrategyExtensionPoint } from "@checkstack/notification-backend";
|
|
12
|
+
import { pluginMetadata } from "./plugin-metadata";
|
|
13
|
+
|
|
14
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
// Configuration Schemas
|
|
16
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Admin configuration for Slack strategy.
|
|
20
|
+
* Optional - no admin config required since users provide their own webhooks.
|
|
21
|
+
*/
|
|
22
|
+
const slackConfigSchemaV1 = z.object({});
|
|
23
|
+
|
|
24
|
+
type SlackConfig = z.infer<typeof slackConfigSchemaV1>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* User configuration for Slack - users provide their webhook URL.
|
|
28
|
+
*/
|
|
29
|
+
const slackUserConfigSchema = z.object({
|
|
30
|
+
webhookUrl: configString({}).url().describe("Slack Incoming Webhook URL"),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type SlackUserConfig = z.infer<typeof slackUserConfigSchema>;
|
|
34
|
+
|
|
35
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
36
|
+
// Instructions
|
|
37
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
38
|
+
|
|
39
|
+
const adminInstructions = `
|
|
40
|
+
## Slack Notifications
|
|
41
|
+
|
|
42
|
+
Slack notifications are delivered via incoming webhooks that users configure individually.
|
|
43
|
+
Each user provides their own webhook URL in their notification settings.
|
|
44
|
+
|
|
45
|
+
No admin configuration is required for this strategy.
|
|
46
|
+
`.trim();
|
|
47
|
+
|
|
48
|
+
const userInstructions = `
|
|
49
|
+
## Create a Slack Incoming Webhook
|
|
50
|
+
|
|
51
|
+
1. Go to [Slack API Apps](https://api.slack.com/apps) and create a new app (or select existing)
|
|
52
|
+
2. Under **Features**, click **Incoming Webhooks** and toggle it **On**
|
|
53
|
+
3. Click **Add New Webhook to Workspace**
|
|
54
|
+
4. Select a channel where you want to receive notifications
|
|
55
|
+
5. Copy the **Webhook URL** and paste it in the field above
|
|
56
|
+
|
|
57
|
+
> **Tip**: You can create webhooks for private channels or your own DM channel for personal notifications.
|
|
58
|
+
`.trim();
|
|
59
|
+
|
|
60
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
61
|
+
// Slack Block Kit Builder
|
|
62
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
63
|
+
|
|
64
|
+
interface SlackBlockOptions {
|
|
65
|
+
title: string;
|
|
66
|
+
body?: string;
|
|
67
|
+
importance: "info" | "warning" | "critical";
|
|
68
|
+
action?: { label: string; url: string };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface SlackPayload {
|
|
72
|
+
text: string; // Fallback text for notifications
|
|
73
|
+
blocks: Array<Record<string, unknown>>;
|
|
74
|
+
attachments?: Array<{ color: string }>;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildSlackPayload(options: SlackBlockOptions): SlackPayload {
|
|
78
|
+
const { title, body, importance, action } = options;
|
|
79
|
+
|
|
80
|
+
const importanceEmoji: Record<string, string> = {
|
|
81
|
+
info: "ℹ️",
|
|
82
|
+
warning: "⚠️",
|
|
83
|
+
critical: "🚨",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Attachment colors for importance-based accent
|
|
87
|
+
const importanceColors: Record<string, string> = {
|
|
88
|
+
info: "#3b82f6", // Blue
|
|
89
|
+
warning: "#f59e0b", // Amber
|
|
90
|
+
critical: "#ef4444", // Red
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const blocks: Array<Record<string, unknown>> = [
|
|
94
|
+
// Header section with title
|
|
95
|
+
{
|
|
96
|
+
type: "section",
|
|
97
|
+
text: {
|
|
98
|
+
type: "mrkdwn",
|
|
99
|
+
text: `${importanceEmoji[importance]} *${title}*`,
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
// Body section (if provided)
|
|
105
|
+
if (body) {
|
|
106
|
+
const mrkdwnBody = markdownToSlackMrkdwn(body);
|
|
107
|
+
blocks.push({
|
|
108
|
+
type: "section",
|
|
109
|
+
text: {
|
|
110
|
+
type: "mrkdwn",
|
|
111
|
+
text: mrkdwnBody,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Action button (if provided)
|
|
117
|
+
if (action?.url) {
|
|
118
|
+
blocks.push({
|
|
119
|
+
type: "actions",
|
|
120
|
+
elements: [
|
|
121
|
+
{
|
|
122
|
+
type: "button",
|
|
123
|
+
text: {
|
|
124
|
+
type: "plain_text",
|
|
125
|
+
text: action.label,
|
|
126
|
+
emoji: true,
|
|
127
|
+
},
|
|
128
|
+
url: action.url,
|
|
129
|
+
action_id: "notification_action",
|
|
130
|
+
},
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
text: `${importanceEmoji[importance]} ${title}`, // Fallback for notifications
|
|
137
|
+
blocks,
|
|
138
|
+
attachments: [{ color: importanceColors[importance] }],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
143
|
+
// Slack Strategy Implementation
|
|
144
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Slack notification strategy using incoming webhooks.
|
|
148
|
+
*/
|
|
149
|
+
const slackStrategy: NotificationStrategy<SlackConfig, SlackUserConfig> = {
|
|
150
|
+
id: "slack",
|
|
151
|
+
displayName: "Slack",
|
|
152
|
+
description: "Send notifications via Slack incoming webhooks",
|
|
153
|
+
icon: "Hash",
|
|
154
|
+
|
|
155
|
+
config: new Versioned({
|
|
156
|
+
version: 1,
|
|
157
|
+
schema: slackConfigSchemaV1,
|
|
158
|
+
}),
|
|
159
|
+
|
|
160
|
+
// User-config resolution - users enter their webhook URL
|
|
161
|
+
contactResolution: { type: "user-config", field: "webhookUrl" },
|
|
162
|
+
|
|
163
|
+
userConfig: new Versioned({
|
|
164
|
+
version: 1,
|
|
165
|
+
schema: slackUserConfigSchema,
|
|
166
|
+
}),
|
|
167
|
+
|
|
168
|
+
adminInstructions,
|
|
169
|
+
userInstructions,
|
|
170
|
+
|
|
171
|
+
async send(
|
|
172
|
+
context: NotificationSendContext<SlackConfig, SlackUserConfig>,
|
|
173
|
+
): Promise<NotificationDeliveryResult> {
|
|
174
|
+
const { userConfig, notification, logger } = context;
|
|
175
|
+
|
|
176
|
+
if (!userConfig?.webhookUrl) {
|
|
177
|
+
return {
|
|
178
|
+
success: false,
|
|
179
|
+
error: "User has not configured their Slack webhook URL",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
// Build the Slack payload
|
|
185
|
+
const payload = buildSlackPayload({
|
|
186
|
+
title: notification.title,
|
|
187
|
+
body: notification.body,
|
|
188
|
+
importance: notification.importance,
|
|
189
|
+
action: notification.action,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Send to Slack webhook
|
|
193
|
+
const response = await fetch(userConfig.webhookUrl, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify(payload),
|
|
199
|
+
signal: AbortSignal.timeout(10_000),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!response.ok) {
|
|
203
|
+
const errorText = await response.text();
|
|
204
|
+
logger.error("Failed to send Slack message", {
|
|
205
|
+
status: response.status,
|
|
206
|
+
error: errorText.slice(0, 500),
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
success: false,
|
|
210
|
+
error: `Failed to send Slack message: ${response.status}`,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Slack webhooks return "ok" on success
|
|
215
|
+
return {
|
|
216
|
+
success: true,
|
|
217
|
+
};
|
|
218
|
+
} catch (error) {
|
|
219
|
+
const message =
|
|
220
|
+
error instanceof Error ? error.message : "Unknown Slack API error";
|
|
221
|
+
logger.error("Slack notification error", { error: message });
|
|
222
|
+
return {
|
|
223
|
+
success: false,
|
|
224
|
+
error: `Failed to send Slack notification: ${message}`,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
231
|
+
// Plugin Definition
|
|
232
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
233
|
+
|
|
234
|
+
export default createBackendPlugin({
|
|
235
|
+
metadata: pluginMetadata,
|
|
236
|
+
|
|
237
|
+
register(env) {
|
|
238
|
+
// Get the notification strategy extension point
|
|
239
|
+
const extensionPoint = env.getExtensionPoint(
|
|
240
|
+
notificationStrategyExtensionPoint,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// Register the Slack strategy with our plugin metadata
|
|
244
|
+
extensionPoint.addStrategy(slackStrategy, pluginMetadata);
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Export for testing
|
|
249
|
+
export { slackConfigSchemaV1, slackUserConfigSchema, buildSlackPayload };
|
|
250
|
+
export type { SlackConfig, SlackUserConfig, SlackBlockOptions, SlackPayload };
|
package/tsconfig.json
ADDED