diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 4a209b6fa..0920b30bb 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -13,7 +13,10 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
- node-version: [18.x, 20.x, 22.x]
+ node-version:
+ - 20.x
+ - 22.x
+ - 24.x
package:
- cli-hooks
- cli-test
@@ -87,20 +90,20 @@ jobs:
id: check_coverage
uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0
with:
- files: packages/${{ matrix.package }}/coverage/lcov.info
+ files: packages/${{ matrix.package }}/**/lcov.info
- name: Upload code coverage
- if: matrix.node-version == '22.x' && matrix.os == 'ubuntu-latest' && steps.check_coverage.outputs.files_exists == 'true'
+ if: matrix.node-version == '24.x' && matrix.os == 'ubuntu-latest' && steps.check_coverage.outputs.files_exists == 'true'
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- directory: packages/${{ matrix.package }}/coverage
+ files: packages/${{ matrix.package }}/coverage/test-results.xml,packages/${{ matrix.package}}/lcov.info # TODO: use "lcov.info" as "file"
flags: ${{ matrix.package }}
verbose: true
- name: Upload test results to Codecov
if: ${{ !cancelled() }}
uses: codecov/test-results-action@47f89e9acb64b76debcd5ea40642d25a4adced9f # v1.1.1
with:
- file: packages/${{ matrix.package }}/coverage/test-results.xml
+ files: packages/${{ matrix.package }}/coverage/test-results.xml,packages/${{ matrix.package}}/lcov.info # TODO: use "lcov.info" as "file"
flags: ${{ matrix.node-version }},${{ matrix.os }},${{ matrix.package }}
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true
diff --git a/packages/webhook/.gitignore b/packages/webhook/.gitignore
index b024219cb..93c46d7e3 100644
--- a/packages/webhook/.gitignore
+++ b/packages/webhook/.gitignore
@@ -6,4 +6,4 @@
/dist
# coverage
-/coverage
+/lcov.info
diff --git a/packages/webhook/README.md b/packages/webhook/README.md
index 5461a2819..653e9b204 100644
--- a/packages/webhook/README.md
+++ b/packages/webhook/README.md
@@ -2,13 +2,11 @@
[](https://codecov.io/gh/slackapi/node-slack-sdk)
-The `@slack/webhook` package contains a helper for making requests to Slack's [Incoming
-Webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks). Use it in your app to send a notification to a channel.
+The `@slack/webhook` package contains a helper for sending message to Slack using [incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks). Use it in your app to send a notification to a channel.
## Requirements
-This package supports Node v18 and higher. It's highly recommended to use [the latest LTS version of
-node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features
-from that version.
+
+This package supports Node v20 and higher. It's highly recommended to use [the latest LTS version of node](https://github.com/nodejs/Release#release-schedule), and the documentation is written using syntax and features from that version.
## Installation
@@ -24,12 +22,10 @@ $ npm install @slack/webhook
### Initialize the webhook
-The package exports an `IncomingWebhook` class. You'll need to initialize it with the URL you received from Slack.
-To create a webhook URL, follow the instructions in the [Getting started with Incoming Webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks)
-guide.
+The package exports an `IncomingWebhook` class. You'll need to initialize it with the URL you received from Slack. To create a webhook URL, follow the instructions in the [Getting started with incoming webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks) guide.
```javascript
-const { IncomingWebhook } = require('@slack/webhook');
+import { IncomingWebhook } from "@slack/webhook";
// Read a url from the environment variables
const url = process.env.SLACK_WEBHOOK_URL;
@@ -43,16 +39,16 @@ const webhook = new IncomingWebhook(url);
Setting default arguments
-The webhook can be initialized with default arguments that are reused each time a notification is sent. Use the second
-parameter to the constructor to set the default arguments.
+The webhook can be initialized with default arguments that are reused each time a notification is sent. Use the second parameter to the constructor to set the default arguments.
```javascript
-const { IncomingWebhook } = require('@slack/webhook');
+import { IncomingWebhook } from "@slack/webhook";
+
const url = process.env.SLACK_WEBHOOK_URL;
// Initialize with defaults
const webhook = new IncomingWebhook(url, {
- icon_emoji: ':bowtie:',
+ unfurl_media: false,
});
```
@@ -62,12 +58,11 @@ const webhook = new IncomingWebhook(url, {
### Send a notification
-Something interesting just happened in your app, so it's time to send the notification! Just call the
-`.send(options)` method on the webhook. The `options` parameter is an object that should describe the contents of
-the message. The method returns a `Promise` that resolves once the notification is sent.
+Something interesting just happened in your app, so it's time to send the notification! Just call the `.send(options)` method on the webhook. The `options` parameter is an object that should describe the contents of the message. The method returns a `Promise` that resolves once the notification is sent.
```javascript
-const { IncomingWebhook } = require('@slack/webhook');
+import { IncomingWebhook } from "@slack/webhook";
+
const url = process.env.SLACK_WEBHOOK_URL;
const webhook = new IncomingWebhook(url);
@@ -75,47 +70,65 @@ const webhook = new IncomingWebhook(url);
// Send the notification
(async () => {
await webhook.send({
- text: 'I\'ve got news for you...',
+ text: "I've got news for you...",
});
})();
```
---
-### Proxy requests with a custom agent
+### Send requests with a custom fetch adapter
-The webhook allows you to customize the HTTP
-[`Agent`](https://nodejs.org/docs/latest/api/http.html#http_class_http_agent) used to create the connection to Slack.
-Using this option is the best way to make all requests from your app go through a proxy, which is a common requirement in
-many corporate settings.
+The `@slack/webhook` package sends requests using [`globalThis.fetch`](https://nodejs.org/api/globals.html#fetch) by default, but you can customize that for various purposes such as for custom handling of retries or proxying requests, both of which are common requirements in corporate settings.
-In order to create an `Agent` from some proxy information (such as a host, port, username, and password), you can use
-one of many npm packages. We recommend [`https-proxy-agent`](https://www.npmjs.com/package/https-proxy-agent). Start
-by installing this package and saving it to your `package.json`.
+In order to use a custom fetch adapter, provide a function that's compatible with the `fetch` interface.
+
+The following example uses the [`undici`](https://www.npmjs.com/package/undici) package to create a dispatcher for proxying requests with a limited timeout. Start by installing this package:
```shell
-$ npm install https-proxy-agent
+$ npm install undici
```
-Import the `HttpsProxyAgent` class, and create an instance that can be used as the `agent` option of the
-`IncomingWebhook`.
+Then import the `ProxyAgent` and `fetch` class from the `undici` package to create a custom `fetch` implementation. This is passed to the `IncomingWebhook` constructor and used in requests:
```javascript
-const { IncomingWebhook } = require('@slack/webhook');
-const { HttpsProxyAgent } = require('https-proxy-agent');
+import { IncomingWebhook } from "@slack/webhook";
+import { ProxyAgent, fetch as undiciFetch } from "undici";
+
const url = process.env.SLACK_WEBHOOK_URL;
-// One of the ways you can configure HttpsProxyAgent is using a simple string.
-// See: https://github.com/TooTallNate/node-https-proxy-agent for more options
-const proxy = new HttpsProxyAgent(process.env.http_proxy || 'http://168.63.76.32:3128');
+/**
+ * Configure your proxy agent here
+ * @see {@link https://undici.nodejs.org/#/docs/api/ProxyAgent.md}
+ */
+const proxyAgent = new ProxyAgent({
+ uri: new URL("http://localhost:8888"),
+ proxyTls: {
+ signal: AbortSignal.timeout(400),
+ },
+});
+
+/**
+ * Implement a custom fetch adapter
+ * @type {typeof globalThis.fetch}
+ * @see {@link https://undici.nodejs.org/#/docs/api/Fetch.md}
+ */
+const myFetch = async (url, opts) => {
+ return await undiciFetch(url, {
+ ...opts,
+ dispatcher: proxyAgent,
+ });
+};
-// Initialize with the proxy agent option
-const webhook = new IncomingWebhook(token, { agent: proxy });
+// Initialize with the custom fetch adapater and proxy
+const webhook = new IncomingWebhook(url, {
+ fetch: myFetch,
+});
// Sending this webhook will now go through the proxy
(async () => {
await webhook.send({
- text: 'I\'ve got news for you...',
+ text: "I've got news for you...",
});
})();
```
diff --git a/packages/webhook/package.json b/packages/webhook/package.json
index 98ab5065c..488f1823a 100644
--- a/packages/webhook/package.json
+++ b/packages/webhook/package.json
@@ -1,9 +1,11 @@
{
+ "$schema": "https://www.schemastore.org/package.json",
"name": "@slack/webhook",
"version": "7.0.6",
"description": "Official library for using the Slack Platform's Incoming Webhooks",
"author": "Slack Technologies, LLC",
"license": "MIT",
+ "type": "module",
"keywords": [
"slack",
"request",
@@ -12,14 +14,14 @@
"api",
"proxy"
],
- "main": "dist/index.js",
+ "main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist/**/*"
],
"engines": {
- "node": ">= 18",
- "npm": ">= 8.6.0"
+ "node": ">= 20",
+ "npm": ">= 9.6.4"
},
"repository": "slackapi/node-slack-sdk",
"homepage": "https://docs.slack.dev/tools/node-slack-sdk/webhook/",
@@ -30,36 +32,28 @@
"url": "https://github.com/slackapi/node-slack-sdk/issues"
},
"scripts": {
- "prepare": "npm run build",
- "build": "npm run build:clean && tsc",
- "build:clean": "shx rm -rf ./dist ./coverage",
+ "prebuild": "shx rm -rf ./dist ./lcov.info",
+ "build": "tsc",
"docs": "npx typedoc --plugin typedoc-plugin-markdown",
"lint": "npx @biomejs/biome check .",
"lint:fix": "npx @biomejs/biome check --write .",
- "mocha": "mocha --config ./test/.mocharc.json src/*.spec.ts",
- "test": "npm run lint && npm run test:unit",
- "test:unit": "npm run build && c8 --config ./test/.c8rc.json npm run mocha"
- },
- "dependencies": {
- "@slack/types": "^2.9.0",
- "@types/node": ">=18.0.0",
- "axios": "^1.11.0"
+ "prepack": "npm run build",
+ "pretest": "npm run lint && npm run build",
+ "test": "node --import tsx --test --experimental-test-coverage",
+ "posttest": "node --import tsx --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=./lcov.info"
},
"devDependencies": {
"@biomejs/biome": "^2.0.5",
- "@types/chai": "^4.3.5",
- "@types/mocha": "^10.0.1",
- "c8": "^10.1.3",
- "chai": "^4.3.8",
- "mocha": "^11.7.1",
- "mocha-junit-reporter": "^2.2.1",
- "mocha-multi-reporters": "^1.5.1",
- "nock": "^14.0.6",
+ "@types/node": ">=20.0.0",
"shx": "^0.4.0",
"source-map-support": "^0.5.21",
- "ts-node": "^10.9.2",
+ "tsx": "^4.20.6",
"typedoc": "^0.28.7",
"typedoc-plugin-markdown": "^4.7.1",
- "typescript": "^5.8.3"
+ "typescript": "^5.8.3",
+ "undici": "^7.16.0"
+ },
+ "peerDependencies": {
+ "@slack/types": "^2.17.0"
}
}
diff --git a/packages/webhook/src/IncomingWebhook.spec.ts b/packages/webhook/src/IncomingWebhook.spec.ts
deleted file mode 100644
index 99bc08e7c..000000000
--- a/packages/webhook/src/IncomingWebhook.spec.ts
+++ /dev/null
@@ -1,115 +0,0 @@
-import { assert } from 'chai';
-import nock from 'nock';
-
-import { ErrorCode } from './errors';
-import { IncomingWebhook } from './IncomingWebhook';
-
-const url = 'https://hooks.slack.com/services/FAKEWEBHOOK';
-
-describe('IncomingWebhook', () => {
- afterEach(() => {
- nock.cleanAll();
- });
-
- describe('constructor()', () => {
- it('should build a default webhook given a URL', () => {
- const webhook = new IncomingWebhook(url);
- assert.instanceOf(webhook, IncomingWebhook);
- });
-
- it('should create a default webhook with a default timeout', () => {
- const webhook = new IncomingWebhook(url);
- assert.nestedPropertyVal(webhook, 'defaults.timeout', 0);
- });
-
- it('should create an axios instance that has the timeout passed by the user', () => {
- const givenTimeout = 100;
- const webhook = new IncomingWebhook(url, { timeout: givenTimeout });
- assert.nestedPropertyVal(webhook, 'axios.defaults.timeout', givenTimeout);
- });
- });
-
- describe('send()', () => {
- let webhook: IncomingWebhook;
- beforeEach(() => {
- webhook = new IncomingWebhook(url);
- });
-
- describe('when making a successful call', () => {
- let scope: nock.Scope;
- beforeEach(() => {
- scope = nock('https://hooks.slack.com')
- .post(/services/)
- .reply(200, 'ok');
- });
-
- it('should return results in a Promise', async () => {
- const result = await webhook.send('Hello');
- assert.strictEqual(result.text, 'ok');
- scope.done();
- });
-
- it('should send metadata', async () => {
- const result = await webhook.send({
- text: 'Hello',
- metadata: {
- event_type: 'foo',
- event_payload: { foo: 'bar' },
- },
- });
- assert.strictEqual(result.text, 'ok');
- scope.done();
- });
- });
-
- describe('when the call fails', () => {
- let statusCode: number;
- let scope: nock.Scope;
- beforeEach(() => {
- statusCode = 500;
- scope = nock('https://hooks.slack.com')
- .post(/services/)
- .reply(statusCode);
- });
-
- it('should return a Promise which rejects on error', async () => {
- try {
- await webhook.send('Hello');
- assert.fail('expected rejection');
- } catch (error) {
- assert.ok(error);
- assert.instanceOf(error, Error);
- assert.match((error as Error).message, new RegExp(String(statusCode)));
- scope.done();
- }
- });
-
- it('should fail with IncomingWebhookRequestError when the API request fails', async () => {
- // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than
- // using nock, we send the request to a host:port that is not listening.
- const webhook = new IncomingWebhook('https://localhost:8999/api/');
- try {
- await webhook.send('Hello');
- assert.fail('expected rejection');
- } catch (error) {
- assert.instanceOf(error, Error);
- assert.propertyVal(error, 'code', ErrorCode.RequestError);
- }
- });
- });
-
- describe('lifecycle', () => {
- it('should not overwrite the default parameters after a call', async () => {
- const defaultParams = { channel: 'default' };
- const webhook = new IncomingWebhook(url, defaultParams);
-
- try {
- await webhook.send({ channel: 'different' });
- assert.fail('expected rejection');
- } catch (_err) {
- assert.nestedPropertyVal(webhook, 'defaults.channel', defaultParams.channel);
- }
- });
- });
- });
-});
diff --git a/packages/webhook/src/IncomingWebhook.test.ts b/packages/webhook/src/IncomingWebhook.test.ts
new file mode 100644
index 000000000..d47e3e56a
--- /dev/null
+++ b/packages/webhook/src/IncomingWebhook.test.ts
@@ -0,0 +1,174 @@
+import assert from 'node:assert';
+import { beforeEach, describe, it } from 'node:test';
+import { MockAgent, setGlobalDispatcher } from 'undici';
+import { IncomingWebhook } from './IncomingWebhook.js';
+
+describe('IncomingWebhook', () => {
+ /**
+ * @description A mock incoming messages webhook.
+ */
+ const url = 'https://hooks.slack.com/services/FAKEWEBHOOK';
+
+ /**
+ * @description A mock HTTP server agent.
+ * @see {@link https://nodejs.org/en/learn/test-runner/mocking#apis}
+ */
+ let agent: MockAgent;
+
+ beforeEach(() => {
+ agent = new MockAgent();
+ setGlobalDispatcher(agent);
+ });
+
+ describe('constructor()', () => {
+ it('should build a default webhook given a URL', () => {
+ const webhook = new IncomingWebhook(url);
+ assert(webhook instanceof IncomingWebhook);
+ });
+ });
+
+ describe('send()', () => {
+ let webhook: IncomingWebhook;
+ beforeEach(() => {
+ webhook = new IncomingWebhook(url);
+ });
+
+ describe('when making a successful call', () => {
+ beforeEach(() => {
+ agent
+ .get('https://hooks.slack.com')
+ .intercept({
+ path: /services/,
+ method: 'POST',
+ })
+ .reply(200, 'ok');
+ });
+
+ it('should return results in a Promise', async () => {
+ const result = await webhook.send('Hello');
+ assert.strictEqual(result.text, 'ok');
+ });
+
+ it('should send metadata', async () => {
+ const result = await webhook.send({
+ text: 'Hello',
+ metadata: {
+ event_type: 'foo',
+ event_payload: { foo: 'bar' },
+ },
+ });
+ assert.strictEqual(result.text, 'ok');
+ });
+ });
+
+ describe('when the call fails', () => {
+ it('should return a Promise which rejects on error', async () => {
+ agent
+ .get('https://hooks.slack.com')
+ .intercept({
+ path: /services/,
+ method: 'POST',
+ })
+ .replyWithError(new Error('500'));
+ try {
+ await webhook.send('Hello');
+ assert.fail('expected rejection');
+ } catch (error) {
+ assert.ok(error);
+ assert.ok(error instanceof Error);
+ assert.match((error as Error).message, /fetch failed/);
+ }
+ });
+
+ it('should fail with IncomingWebhookRequestError when the API request fails', async () => {
+ // One known request error is when the node encounters an ECONNREFUSED. In order to simulate this, rather than
+ // using mocks, we send the request to a host:port that is not listening.
+ const webhook = new IncomingWebhook('https://localhost:8999/api/');
+ try {
+ await webhook.send('Hello');
+ assert.fail('expected rejection');
+ } catch (error) {
+ assert.ok(error instanceof Error);
+ }
+ });
+ });
+
+ describe('lifecycle', () => {
+ it('should send to the provided fetch handler', async () => {
+ let actualUrl: URL | RequestInfo | undefined;
+ let actualText: string | undefined;
+ const mockFetch: typeof globalThis.fetch = async (url, body) => {
+ const mockBody = JSON.parse(body?.body?.toString() || '{}');
+ actualUrl = url;
+ actualText = mockBody.text;
+ return Promise.resolve(new Response(JSON.stringify({ text: 'ok' })));
+ };
+ const webhook = new IncomingWebhook(url, {
+ fetch: mockFetch,
+ });
+ await webhook.send({ text: 'updates' });
+ assert.strictEqual(actualText, 'updates');
+ assert.strictEqual(actualUrl, url);
+ });
+
+ it('should not overwrite the default parameters after a call', async () => {
+ const mockResponse1 = 'ok+1';
+ const mockResponse2 = 'ok+2';
+ agent
+ .get('https://hooks.slack.com')
+ .intercept({
+ path: /services/,
+ method: 'POST',
+ body: JSON.stringify({
+ text: 'A sheep appeared!',
+ metadata: {
+ event_type: 'count',
+ event_payload: {
+ sheep: 1,
+ },
+ },
+ }),
+ })
+ .reply(200, mockResponse1);
+ agent
+ .get('https://hooks.slack.com')
+ .intercept({
+ path: /services/,
+ method: 'POST',
+ body: JSON.stringify({
+ text: 'A sheep appeared!',
+ metadata: {
+ event_type: 'count',
+ event_payload: {
+ sheep: 2,
+ },
+ },
+ }),
+ })
+ .reply(200, mockResponse2);
+ const defaultParams = {
+ text: 'A sheep appeared!',
+ };
+ const webhook = new IncomingWebhook(url, defaultParams);
+ const response1 = await webhook.send({
+ metadata: {
+ event_type: 'count',
+ event_payload: {
+ sheep: 1,
+ },
+ },
+ });
+ const response2 = await webhook.send({
+ metadata: {
+ event_type: 'count',
+ event_payload: {
+ sheep: 2,
+ },
+ },
+ });
+ assert.strictEqual(response1.text, mockResponse1);
+ assert.strictEqual(response2.text, mockResponse2);
+ });
+ });
+ });
+});
diff --git a/packages/webhook/src/IncomingWebhook.ts b/packages/webhook/src/IncomingWebhook.ts
index 8b5046121..bebb86ecd 100644
--- a/packages/webhook/src/IncomingWebhook.ts
+++ b/packages/webhook/src/IncomingWebhook.ts
@@ -1,125 +1,137 @@
-import type { Agent } from 'node:http';
-
-import type { Block, KnownBlock, MessageAttachment } from '@slack/types'; // TODO: Block and KnownBlock will be merged into AnyBlock in upcoming types release
-import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
-
-import { httpErrorWithOriginal, requestErrorWithOriginal } from './errors';
-import { getUserAgent } from './instrument';
+import type { AnyBlock, MessageAttachment, MessageMetadata } from '@slack/types';
+import { httpErrorWithOriginal, requestErrorWithOriginal } from './errors.js';
+import { getUserAgent } from './instrument.js';
/**
- * A client for Slack's Incoming Webhooks
+ * @description A client to send messages with Slack incoming webhooks.
+ * @see {@link https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks}
*/
export class IncomingWebhook {
/**
- * The webhook URL
+ * @description The webhook URL.
*/
private url: string;
/**
- * Default arguments for posting messages with this webhook
+ * @description Default arguments for posting messages with this webhook.
*/
- private defaults: IncomingWebhookDefaultArguments;
+ private defaults: IncomingWebhookSendArguments;
/**
- * Axios HTTP client instance used by this client
+ * @description A method to send requests.
+ * @see {@link https://github.com/nodejs/undici/discussions/2167}
*/
- private axios: AxiosInstance;
+ private fetch: typeof globalThis.fetch;
- public constructor(
- url: string,
- defaults: IncomingWebhookDefaultArguments = {
- timeout: 0,
- },
- ) {
- if (url === undefined) {
- throw new Error('Incoming webhook URL is required');
- }
-
- this.url = url;
+ /**
+ * @param url The URL of the incoming webhook.
+ * @param configuration Options and default arguments used when sending a message.
+ */
+ public constructor(url: string, configuration: IncomingWebhookOptions & IncomingWebhookSendArguments = {}) {
+ const { fetch, ...defaults } = configuration;
this.defaults = defaults;
-
- this.axios = axios.create({
- baseURL: url,
- httpAgent: defaults.agent,
- httpsAgent: defaults.agent,
- maxRedirects: 0,
- proxy: false,
- timeout: defaults.timeout,
- headers: {
- 'User-Agent': getUserAgent(),
- },
- });
-
- this.defaults.agent = undefined;
+ this.fetch = fetch || globalThis.fetch;
+ this.url = url;
}
/**
- * Send a notification to a conversation
+ * Send a notification to a conversation.
* @param message - the message (a simple string, or an object describing the message)
*/
public async send(message: string | IncomingWebhookSendArguments): Promise {
- // NOTE: no support for TLS config
- let payload: IncomingWebhookSendArguments = { ...this.defaults };
-
+ let payload: IncomingWebhookSendArguments = {
+ ...this.defaults,
+ };
if (typeof message === 'string') {
payload.text = message;
} else {
payload = Object.assign(payload, message);
}
-
try {
- const response = await this.axios.post(this.url, payload);
- return this.buildResult(response);
+ const response = await this.fetch(this.url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ 'User-Agent': getUserAgent(),
+ },
+ body: JSON.stringify(payload),
+ });
+ const text = await response.text();
+ const result = {
+ text,
+ };
+ return result;
// biome-ignore lint/suspicious/noExplicitAny: errors can be anything
} catch (error: any) {
// Wrap errors in this packages own error types (abstract the implementation details' types)
if (error.response !== undefined) {
throw httpErrorWithOriginal(error);
}
- if (error.request !== undefined) {
+ if (error.request !== undefined || error.code) {
throw requestErrorWithOriginal(error);
}
throw error;
}
}
+}
+/**
+ * @description Configuration options used to send incoming webhooks.
+ */
+export interface IncomingWebhookOptions {
/**
- * Processes an HTTP response into an IncomingWebhookResult.
+ * @description A method to send requests.
+ * @see {@link https://github.com/nodejs/undici/discussions/2167}
*/
- private buildResult(response: AxiosResponse): IncomingWebhookResult {
- return {
- text: response.data,
- };
- }
+ fetch?: typeof globalThis.fetch;
}
-/*
- * Exported types
+/**
+ * @description Arguments to use when sending a message using an incoming webhook.
+ * @see {@link https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks}
*/
-
-export interface IncomingWebhookDefaultArguments {
- username?: string;
- icon_emoji?: string;
- icon_url?: string;
- channel?: string;
- text?: string;
- link_names?: boolean;
- agent?: Agent;
- timeout?: number;
-}
-
-export interface IncomingWebhookSendArguments extends IncomingWebhookDefaultArguments {
+export interface IncomingWebhookSendArguments {
+ /**
+ * @description Add {@link https://docs.slack.dev/messaging/formatting-message-text#attachments secondary attachments} to your messages in Slack. Message attachments are considered a legacy part of messaging functionality. They are not deprecated per se, but they may change in the future, in ways that reduce their visibility or utility. We recommend moving to Block Kit instead. Read more about {@link https://docs.slack.dev/messaging/formatting-message-text#attachments when to use message attachments}.
+ * @see {@link https://docs.slack.dev/messaging/formatting-message-text#attachmentsSecondary message attachments reference documentation}
+ */
attachments?: MessageAttachment[];
- blocks?: (KnownBlock | Block)[];
+ /**
+ * @description Add {@link https://docs.slack.dev/block-kit/ blocks} as a visual components to arrange message layouts.
+ * @see {@link https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts}
+ * @see {@link https://docs.slack.dev/block-kit/}
+ * @see {@link https://docs.slack.dev/block-kit/formatting-with-rich-text/}
+ */
+ blocks?: AnyBlock[];
+ /**
+ * @description Text to send to the incoming webhook. Formatted as {@link https://docs.slack.dev/messaging/formatting-message-text/#basic-formatting mrkdwn}.
+ * @see {@link https://docs.slack.dev/messaging/formatting-message-text/}
+ */
+ text?: string;
+ /**
+ * @description Pass `true` to enable unfurling of primarily text-based content.
+ * @default false
+ * @see {@link https://docs.slack.dev/messaging/unfurling-links-in-messages#classic_unfurl}
+ */
unfurl_links?: boolean;
+ /**
+ * @description Pass `false` to disable unfurling of media content.
+ * @default true
+ * @see {@link https://docs.slack.dev/messaging/unfurling-links-in-messages#classic_unfurl}
+ **/
unfurl_media?: boolean;
- metadata?: {
- event_type: string;
- // biome-ignore lint/suspicious/noExplicitAny: errors can be anything
- event_payload: Record;
- };
+ /**
+ * @description Object representing message metadata, which will be made accessible to any user or app.
+ * @see {@link https://docs.slack.dev/messaging/message-metadata/}
+ **/
+ metadata?: MessageMetadata;
}
+/**
+ * The result from a posted incoming webhook.
+ * @example {text:"ok"}
+ * @see {@link https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/#handling_errors}.
+ */
export interface IncomingWebhookResult {
text: string;
}
diff --git a/packages/webhook/src/errors.ts b/packages/webhook/src/errors.ts
index 1252b190a..41aca6c70 100644
--- a/packages/webhook/src/errors.ts
+++ b/packages/webhook/src/errors.ts
@@ -1,5 +1,3 @@
-import type { AxiosError, AxiosResponse } from 'axios';
-
/**
* All errors produced by this package adhere to this interface
*/
@@ -41,7 +39,7 @@ function errorWithCode(error: Error, code: ErrorCode): CodedError {
* A factory to create IncomingWebhookRequestError objects
* @param original The original error
*/
-export function requestErrorWithOriginal(original: AxiosError): IncomingWebhookRequestError {
+export function requestErrorWithOriginal(original: Error): IncomingWebhookRequestError {
const error = errorWithCode(
new Error(`A request error occurred: ${original.message}`),
ErrorCode.RequestError,
@@ -54,7 +52,9 @@ export function requestErrorWithOriginal(original: AxiosError): IncomingWebhookR
* A factory to create IncomingWebhookHTTPError objects
* @param original The original error
*/
-export function httpErrorWithOriginal(original: AxiosError & { response: AxiosResponse }): IncomingWebhookHTTPError {
+export function httpErrorWithOriginal(
+ original: Error & { response: { status: number; statusText: string; data: string } },
+): IncomingWebhookHTTPError {
const error = errorWithCode(
new Error(`An HTTP protocol error occurred: statusCode = ${original.response.status}`),
ErrorCode.HTTPError,
diff --git a/packages/webhook/src/index.ts b/packages/webhook/src/index.ts
index 74420ffba..ca4b085c0 100644
--- a/packages/webhook/src/index.ts
+++ b/packages/webhook/src/index.ts
@@ -1,16 +1,14 @@
-///
-
export {
CodedError,
ErrorCode,
IncomingWebhookHTTPError,
IncomingWebhookRequestError,
IncomingWebhookSendError,
-} from './errors';
+} from './errors.js';
export {
IncomingWebhook,
- IncomingWebhookDefaultArguments,
+ IncomingWebhookOptions,
IncomingWebhookResult,
IncomingWebhookSendArguments,
-} from './IncomingWebhook';
+} from './IncomingWebhook.js';
diff --git a/packages/webhook/src/instrument.ts b/packages/webhook/src/instrument.ts
index 8434d7b24..8d5f7efeb 100644
--- a/packages/webhook/src/instrument.ts
+++ b/packages/webhook/src/instrument.ts
@@ -1,6 +1,6 @@
import * as os from 'node:os';
-const packageJson = require('../package.json');
+import packageJson from '../package.json' with { type: 'json' };
/**
* Replaces occurrences of '/' with ':' in a string, since '/' is meaningful inside User-Agent strings as a separator.
diff --git a/packages/webhook/test/.c8rc.json b/packages/webhook/test/.c8rc.json
deleted file mode 100644
index 94b35acfe..000000000
--- a/packages/webhook/test/.c8rc.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "include": ["src/*.ts"],
- "exclude": ["**/*.spec.js"],
- "reporter": ["lcov", "text"],
- "all": false,
- "cache": true
-}
diff --git a/packages/webhook/test/.mocharc.json b/packages/webhook/test/.mocharc.json
deleted file mode 100644
index 43e157500..000000000
--- a/packages/webhook/test/.mocharc.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "reporter": "mocha-multi-reporters",
- "reporter-options": ["configFile=./test/.reports.json"],
- "require": ["ts-node/register", "source-map-support/register"]
-}
diff --git a/packages/webhook/test/.reports.json b/packages/webhook/test/.reports.json
deleted file mode 100644
index 123d170d6..000000000
--- a/packages/webhook/test/.reports.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "reporterEnabled": "spec, mocha-junit-reporter",
- "mochaJunitReporterReporterOptions": {
- "mochaFile": "./coverage/test-results.xml",
- "rootSuiteTitle": "exists",
- "testsuitesTitle": "@slack/webhook"
- }
-}
diff --git a/packages/webhook/tsconfig.json b/packages/webhook/tsconfig.json
index adb1b18be..e6faed87c 100644
--- a/packages/webhook/tsconfig.json
+++ b/packages/webhook/tsconfig.json
@@ -1,31 +1,24 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
- "target": "es2017",
- "module": "commonjs",
+ "target": "es6",
+ "module": "nodenext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
+ "rootDir": "./src",
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
- "moduleResolution": "node",
- "baseUrl": ".",
- "paths": {
- "*": ["./types/*"]
- },
- "esModuleInterop": true
- // Not using this setting because it's only used to require the package.json file, and that would change the
- // structure of the files in the dist directory because package.json is not located inside src. It would be nice
- // to use import instead of require(), but it's not worth the tradeoff of restructuring the build (for now).
- // "resolveJsonModule": true,
+ "moduleResolution": "nodenext",
+ "esModuleInterop": true,
+ "resolveJsonModule": true
},
- "include": ["src/**/*"],
- "exclude": ["src/**/*.spec.*"],
+ "include": ["package.json", "src/**/*"],
"jsdoc": {
"out": "support/jsdoc",
"access": "public"
diff --git a/packages/webhook/typedoc.json b/packages/webhook/typedoc.json
index 310e82da5..703beff03 100644
--- a/packages/webhook/typedoc.json
+++ b/packages/webhook/typedoc.json
@@ -21,5 +21,5 @@
"jsDocCompatibility": true,
"hidePageHeader": true,
"entryFileName": "index",
- "exclude": ["**/*.test.ts", "**/*.spec.ts", "**/*.test-d.ts"]
+ "exclude": ["**/*.test.ts", "**/*.test-d.ts"]
}