From 43050e0a9ec94d6d42a007d656b4f3ffb0d8ed2f Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 18:30:58 -0400 Subject: [PATCH 01/11] chore: drop support for node 18 (#2927) --- .changeset/drop-node-18.md | 5 ++++ .github/workflows/ci-build.yml | 1 - .github/workflows/samples.yml | 2 -- AGENTS.md | 2 +- examples/custom-receiver/package-lock.json | 10 +++---- examples/custom-receiver/package.json | 2 +- examples/custom-receiver/tsconfig.json | 2 +- .../getting-started-typescript/package.json | 2 +- .../getting-started-typescript/tsconfig.json | 2 +- package-lock.json | 30 +++++++++++-------- package.json | 8 ++--- tsconfig.json | 2 +- 12 files changed, 38 insertions(+), 30 deletions(-) create mode 100644 .changeset/drop-node-18.md diff --git a/.changeset/drop-node-18.md b/.changeset/drop-node-18.md new file mode 100644 index 000000000..6bb2a9043 --- /dev/null +++ b/.changeset/drop-node-18.md @@ -0,0 +1,5 @@ +--- +"@slack/bolt": major +--- + +Drop Node.js 18 support. The minimum required runtime is now Node.js 20 (npm >=9.6.4). diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index 0bf2e9d8b..81c6f31a4 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -20,7 +20,6 @@ jobs: fail-fast: false matrix: node-version: - - 18.x - 20.x - 22.x - 24.x diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml index 66803b183..5657524d0 100644 --- a/.github/workflows/samples.yml +++ b/.github/workflows/samples.yml @@ -17,7 +17,6 @@ jobs: fail-fast: false matrix: node-version: - - 18.x - 20.x - 22.x - 24.x @@ -48,7 +47,6 @@ jobs: fail-fast: false matrix: node-version: - - 18.x - 20.x - 22.x - 24.x diff --git a/AGENTS.md b/AGENTS.md index 697b4af49..32f728f65 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -113,7 +113,7 @@ Listeners receive a single object with these properties (availability depends on ## Code Conventions -- **TypeScript** throughout. Compiler options in `tsconfig.json` (extends `@tsconfig/node18`, CommonJS output). +- **TypeScript** throughout. Compiler options in `tsconfig.json` (extends `@tsconfig/node20`, CommonJS output). - **Biome** for formatting and linting. Configuration in `biome.json`. - **Testing:** See the Testing section below for test frameworks and conventions. diff --git a/examples/custom-receiver/package-lock.json b/examples/custom-receiver/package-lock.json index 2ecda9635..467247383 100644 --- a/examples/custom-receiver/package-lock.json +++ b/examples/custom-receiver/package-lock.json @@ -18,7 +18,7 @@ "koa": "^3" }, "devDependencies": { - "@tsconfig/node18": "^18.2.6", + "@tsconfig/node20": "^20.1.5", "@types/koa": "^3.0.2", "@types/node": "^24", "ts-node": "^10", @@ -341,10 +341,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@tsconfig/node18": { - "version": "18.2.6", - "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-18.2.6.tgz", - "integrity": "sha512-eAWQzAjPj18tKnDzmWstz4OyWewLUNBm9tdoN9LayzoboRktYx3Enk1ZXPmThj55L7c4VWYq/Bzq0A51znZfhw==", + "node_modules/@tsconfig/node20": { + "version": "20.1.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz", + "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==", "dev": true, "license": "MIT" }, diff --git a/examples/custom-receiver/package.json b/examples/custom-receiver/package.json index dfbf3154e..5c915bcc4 100644 --- a/examples/custom-receiver/package.json +++ b/examples/custom-receiver/package.json @@ -20,7 +20,7 @@ "koa": "^3" }, "devDependencies": { - "@tsconfig/node18": "^18.2.6", + "@tsconfig/node20": "^20.1.5", "@types/koa": "^3.0.2", "@types/node": "^24", "ts-node": "^10", diff --git a/examples/custom-receiver/tsconfig.json b/examples/custom-receiver/tsconfig.json index a1d742ea0..e2506334d 100644 --- a/examples/custom-receiver/tsconfig.json +++ b/examples/custom-receiver/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@tsconfig/node18/tsconfig.json", + "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "resolveJsonModule": true, "allowSyntheticDefaultImports": true, diff --git a/examples/getting-started-typescript/package.json b/examples/getting-started-typescript/package.json index 9654c466c..66e8febe4 100644 --- a/examples/getting-started-typescript/package.json +++ b/examples/getting-started-typescript/package.json @@ -14,7 +14,7 @@ "dotenv": "^17" }, "devDependencies": { - "@tsconfig/node18": "^18.2.6", + "@tsconfig/node20": "^20.1.5", "@types/node": "^24", "ts-node": "^10", "typescript": "6.0.3" diff --git a/examples/getting-started-typescript/tsconfig.json b/examples/getting-started-typescript/tsconfig.json index a1d742ea0..e2506334d 100644 --- a/examples/getting-started-typescript/tsconfig.json +++ b/examples/getting-started-typescript/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@tsconfig/node18/tsconfig.json", + "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "resolveJsonModule": true, "allowSyntheticDefaultImports": true, diff --git a/package-lock.json b/package-lock.json index 07fb90d04..bfe15ac60 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,10 +23,10 @@ "devDependencies": { "@biomejs/biome": "^1.9.0", "@changesets/cli": "^2.29.8", - "@tsconfig/node18": "^18.2.4", + "@tsconfig/node20": "^20.1.5", "@types/chai": "^4.1.7", "@types/mocha": "^10.0.1", - "@types/node": "18.19.130", + "@types/node": "20.19.0", "@types/proxyquire": "^1.3.31", "@types/sinon": "^17.0.4", "@types/tsscmp": "^1.0.0", @@ -42,8 +42,8 @@ "typescript": "5.3.3" }, "engines": { - "node": ">=18", - "npm": ">=8.6.0" + "node": ">=20", + "npm": ">=9.6.4" }, "peerDependencies": { "@types/express": "^5.0.0" @@ -1013,8 +1013,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@tsconfig/node18": { - "version": "18.2.6", + "node_modules/@tsconfig/node20": { + "version": "20.1.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node20/-/node20-20.1.9.tgz", + "integrity": "sha512-IjlTv1RsvnPtUcjTqtVsZExKVq+KQx4g5pCP5tI7rAs6Xesl2qFwSz/tPDBC4JajkL/MlezBu3gPUwqRHl+RIg==", "dev": true, "license": "MIT" }, @@ -1121,12 +1123,20 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "18.19.130", + "version": "20.19.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.0.tgz", + "integrity": "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q==", "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.21.0" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@types/normalize-package-data": { "version": "2.4.4", "dev": true, @@ -4563,10 +4573,6 @@ "node": ">=14.17" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "license": "MIT" - }, "node_modules/universalify": { "version": "0.1.2", "dev": true, diff --git a/package.json b/package.json index 4693c3bac..d0cf251be 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,8 @@ "dist/**/*" ], "engines": { - "node": ">=18", - "npm": ">=8.6.0" + "node": ">=20", + "npm": ">=9.6.4" }, "scripts": { "build": "npm run build:clean && tsc", @@ -61,10 +61,10 @@ "devDependencies": { "@biomejs/biome": "^1.9.0", "@changesets/cli": "^2.29.8", - "@tsconfig/node18": "^18.2.4", + "@tsconfig/node20": "^20.1.5", "@types/chai": "^4.1.7", "@types/mocha": "^10.0.1", - "@types/node": "18.19.130", + "@types/node": "20.19.0", "@types/proxyquire": "^1.3.31", "@types/sinon": "^17.0.4", "@types/tsscmp": "^1.0.0", diff --git a/tsconfig.json b/tsconfig.json index 93c49d8eb..533df7dea 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/tsconfig", - "extends": "@tsconfig/node18/tsconfig.json", + "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { "declaration": true, "declarationMap": true, From b439d8f4f3f41c6ba180ba5f8285f28c6e1f876e Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 18:52:40 -0400 Subject: [PATCH 02/11] chore: remove deprecated workflow steps references (#2928) --- .changeset/drop-workflow-steps.md | 5 + src/App.ts | 17 - src/WorkflowStep.ts | 432 ------------------------ src/errors.ts | 11 - src/helpers.ts | 3 +- src/index.ts | 8 - src/types/actions/index.ts | 12 +- src/types/actions/workflow-step-edit.ts | 54 --- src/types/view/index.ts | 38 +-- test/unit/WorkflowStep.spec.ts | 388 --------------------- 10 files changed, 10 insertions(+), 958 deletions(-) create mode 100644 .changeset/drop-workflow-steps.md delete mode 100644 src/WorkflowStep.ts delete mode 100644 src/types/actions/workflow-step-edit.ts delete mode 100644 test/unit/WorkflowStep.spec.ts diff --git a/.changeset/drop-workflow-steps.md b/.changeset/drop-workflow-steps.md new file mode 100644 index 000000000..c2d30a399 --- /dev/null +++ b/.changeset/drop-workflow-steps.md @@ -0,0 +1,5 @@ +--- +"@slack/bolt": major +--- + +Remove deprecated `WorkflowStep` class and all associated types, middleware, and utilities. Use `CustomFunction` and `app.function()` instead. diff --git a/src/App.ts b/src/App.ts index f0e4de9f3..56ba1538b 100644 --- a/src/App.ts +++ b/src/App.ts @@ -11,7 +11,6 @@ import { type FunctionFailFn, type SlackCustomFunctionMiddlewareArgs, } from './CustomFunction'; -import type { WorkflowStep } from './WorkflowStep'; import { createFunctionComplete, createFunctionFail, @@ -93,7 +92,6 @@ import type { SlashCommand, ViewConstraints, ViewOutput, - WorkflowStepEdit, } from './types'; import { contextBuiltinKeys } from './types'; import { type StringIndexed, isRejected } from './types/utilities'; @@ -531,19 +529,6 @@ export default class App return this; } - /** - * Register WorkflowStep middleware - * - * @param workflowStep global workflow step middleware function - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ - public step(workflowStep: WorkflowStep): this { - const m = workflowStep.getMiddleware(); - this.middleware.push(m); - return this; - } - /** * Register middleware for a workflow step. * @param callbackId Unique callback ID of a step. @@ -1017,11 +1002,9 @@ export default class App // Set body and payload // TODO: this value should eventually conform to AnyMiddlewareArgs - // TODO: remove workflow step stuff in bolt v5 // TODO: can we instead use type predicates in these switch cases to allow for narrowing of the body simultaneously? we have isEvent, isView, isShortcut, isAction already in types/utilities / helpers let payload: | DialogSubmitAction - | WorkflowStepEdit | SlackShortcut | KnownEventFromType | SlashCommand diff --git a/src/WorkflowStep.ts b/src/WorkflowStep.ts deleted file mode 100644 index 84f941517..000000000 --- a/src/WorkflowStep.ts +++ /dev/null @@ -1,432 +0,0 @@ -import type { WorkflowStepExecuteEvent } from '@slack/types'; -import type { - Block, - KnownBlock, - ViewsOpenResponse, - WorkflowsStepCompletedResponse, - WorkflowsStepFailedResponse, - WorkflowsUpdateStepResponse, -} from '@slack/web-api'; -import { WorkflowStepInitializationError } from './errors'; -import processMiddleware from './middleware/process'; -import type { - AllMiddlewareArgs, - AnyMiddlewareArgs, - Context, - Middleware, - SlackActionMiddlewareArgs, - SlackEventMiddlewareArgs, - SlackViewMiddlewareArgs, - ViewWorkflowStepSubmitAction, - WorkflowStepEdit, -} from './types'; - -/** Interfaces */ - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface StepConfigureArguments { - blocks: (KnownBlock | Block)[]; - private_metadata?: string; - submit_disabled?: boolean; - external_id?: string; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface StepUpdateArguments { - inputs?: Record< - string, - { - // biome-ignore lint/suspicious/noExplicitAny: user-defined workflow inputs could be anything - value: any; - skip_variable_replacement?: boolean; - // biome-ignore lint/suspicious/noExplicitAny: user-defined workflow inputs could be anything - variables?: Record; - } - >; - outputs?: { - name: string; - type: string; - label: string; - }[]; - step_name?: string; - step_image_url?: string; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface StepCompleteArguments { - // biome-ignore lint/suspicious/noExplicitAny: user-defined workflow outputs could be anything - outputs?: Record; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface StepFailArguments { - error: { - message: string; - }; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type StepConfigureFn = (params: StepConfigureArguments) => Promise; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type StepUpdateFn = (params?: StepUpdateArguments) => Promise; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type StepCompleteFn = (params?: StepCompleteArguments) => Promise; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type StepFailFn = (params: StepFailArguments) => Promise; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface WorkflowStepConfig { - edit: WorkflowStepEditMiddleware | WorkflowStepEditMiddleware[]; - save: WorkflowStepSaveMiddleware | WorkflowStepSaveMiddleware[]; - execute: WorkflowStepExecuteMiddleware | WorkflowStepExecuteMiddleware[]; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface WorkflowStepEditMiddlewareArgs extends SlackActionMiddlewareArgs { - step: WorkflowStepEdit['workflow_step']; - configure: StepConfigureFn; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface WorkflowStepSaveMiddlewareArgs extends SlackViewMiddlewareArgs { - step: ViewWorkflowStepSubmitAction['workflow_step']; - update: StepUpdateFn; -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface WorkflowStepExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'workflow_step_execute'> { - step: WorkflowStepExecuteEvent['workflow_step']; - complete: StepCompleteFn; - fail: StepFailFn; -} - -/** Types */ - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type SlackWorkflowStepMiddlewareArgs = - | WorkflowStepEditMiddlewareArgs - | WorkflowStepSaveMiddlewareArgs - | WorkflowStepExecuteMiddlewareArgs; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type WorkflowStepEditMiddleware = Middleware; -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type WorkflowStepSaveMiddleware = Middleware; -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type WorkflowStepExecuteMiddleware = Middleware; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type WorkflowStepMiddleware = - | WorkflowStepEditMiddleware[] - | WorkflowStepSaveMiddleware[] - | WorkflowStepExecuteMiddleware[]; - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export type AllWorkflowStepMiddlewareArgs = - T & AllMiddlewareArgs; - -/** Constants */ - -const VALID_PAYLOAD_TYPES = new Set(['workflow_step_edit', 'workflow_step', 'workflow_step_execute']); - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export class WorkflowStep { - /** Step callback_id */ - private callbackId: string; - - /** Step Add/Edit :: 'workflow_step_edit' action */ - private edit: WorkflowStepEditMiddleware[]; - - /** Step Config Save :: 'view_submission' */ - private save: WorkflowStepSaveMiddleware[]; - - /** Step Executed/Run :: 'workflow_step_execute' event */ - private execute: WorkflowStepExecuteMiddleware[]; - - public constructor(callbackId: string, config: WorkflowStepConfig) { - validate(callbackId, config); - - const { save, edit, execute } = config; - - this.callbackId = callbackId; - this.save = Array.isArray(save) ? save : [save]; - this.edit = Array.isArray(edit) ? edit : [edit]; - this.execute = Array.isArray(execute) ? execute : [execute]; - } - - public getMiddleware(): Middleware { - return async (args): Promise => { - if (isStepEvent(args) && this.matchesConstraints(args)) { - return this.processEvent(args); - } - return args.next(); - }; - } - - private matchesConstraints(args: SlackWorkflowStepMiddlewareArgs): boolean { - return args.payload.callback_id === this.callbackId; - } - - private async processEvent(args: AllWorkflowStepMiddlewareArgs): Promise { - const { payload } = args; - const stepArgs = prepareStepArgs(args); - const stepMiddleware = this.getStepMiddleware(payload); - return processStepMiddleware(stepArgs, stepMiddleware); - } - - private getStepMiddleware(payload: AllWorkflowStepMiddlewareArgs['payload']): WorkflowStepMiddleware { - switch (payload.type) { - case 'workflow_step_edit': - return this.edit; - case 'workflow_step': - return this.save; - case 'workflow_step_execute': - return this.execute; - default: - return []; - } - } -} - -/** Helper Functions */ - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export function validate(callbackId: string, config: WorkflowStepConfig): void { - // Ensure callbackId is valid - if (typeof callbackId !== 'string') { - const errorMsg = 'WorkflowStep expects a callback_id as the first argument'; - throw new WorkflowStepInitializationError(errorMsg); - } - - // Ensure step config object is passed in - if (typeof config !== 'object') { - const errorMsg = 'WorkflowStep expects a configuration object as the second argument'; - throw new WorkflowStepInitializationError(errorMsg); - } - - // Check for missing required keys - const requiredKeys: (keyof WorkflowStepConfig)[] = ['save', 'edit', 'execute']; - const missingKeys: (keyof WorkflowStepConfig)[] = []; - for (const key of requiredKeys) { - if (config[key] === undefined) { - missingKeys.push(key); - } - } - - if (missingKeys.length > 0) { - const errorMsg = `WorkflowStep is missing required keys: ${missingKeys.join(', ')}`; - throw new WorkflowStepInitializationError(errorMsg); - } - - // Ensure a callback or an array of callbacks is present - const requiredFns: (keyof WorkflowStepConfig)[] = ['save', 'edit', 'execute']; - for (const fn of requiredFns) { - if (typeof config[fn] !== 'function' && !Array.isArray(config[fn])) { - const errorMsg = `WorkflowStep ${fn} property must be a function or an array of functions`; - throw new WorkflowStepInitializationError(errorMsg); - } - } -} - -/** - * `processStepMiddleware()` invokes each callback for lifecycle event - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - * @param args workflow_step_edit action - */ -export async function processStepMiddleware( - args: AllWorkflowStepMiddlewareArgs, - middleware: WorkflowStepMiddleware, -): Promise { - const { context, client, logger } = args; - // TODO :: revisit type used below (look into contravariance) - const callbacks = [...middleware] as Middleware[]; - const lastCallback = callbacks.pop(); - - if (lastCallback !== undefined) { - await processMiddleware(callbacks, args, context, client, logger, async () => - lastCallback({ ...args, context, client, logger }), - ); - } -} - -/** @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export function isStepEvent(args: AnyMiddlewareArgs): args is AllWorkflowStepMiddlewareArgs { - return VALID_PAYLOAD_TYPES.has(args.payload.type); -} - -function selectToken(context: Context): string | undefined { - return context.botToken !== undefined ? context.botToken : context.userToken; -} - -/** - * Factory for `configure()` utility - * @param args workflow_step_edit action - */ -function createStepConfigure(args: AllWorkflowStepMiddlewareArgs): StepConfigureFn { - const { - context, - client, - body: { callback_id, trigger_id }, - } = args; - const token = selectToken(context); - - return (params: Parameters[0]) => - client.views.open({ - token, - trigger_id, - view: { - callback_id, - type: 'workflow_step', - ...params, - }, - }); -} - -/** - * Factory for `update()` utility - * @param args view_submission event - */ -function createStepUpdate(args: AllWorkflowStepMiddlewareArgs): StepUpdateFn { - const { - context, - client, - body: { - workflow_step: { workflow_step_edit_id }, - }, - } = args; - const token = selectToken(context); - - return (params: Parameters[0] = {}) => - client.workflows.updateStep({ - token, - workflow_step_edit_id, - ...params, - }); -} - -/** - * Factory for `complete()` utility - * @param args workflow_step_execute event - */ -function createStepComplete(args: AllWorkflowStepMiddlewareArgs): StepCompleteFn { - const { - context, - client, - payload: { - workflow_step: { workflow_step_execute_id }, - }, - } = args; - const token = selectToken(context); - - return (params: Parameters[0] = {}) => - client.workflows.stepCompleted({ - token, - workflow_step_execute_id, - ...params, - }); -} - -/** - * Factory for `fail()` utility - * @param args workflow_step_execute event - */ -function createStepFail(args: AllWorkflowStepMiddlewareArgs): StepFailFn { - const { - context, - client, - payload: { - workflow_step: { workflow_step_execute_id }, - }, - } = args; - const token = selectToken(context); - - return (params: Parameters[0]) => { - const { error } = params; - return client.workflows.stepFailed({ - token, - workflow_step_execute_id, - error, - }); - }; -} - -/** - * `prepareStepArgs()` takes in a step's args and: - * 1. removes the next() passed in from App-level middleware processing - * - events will *not* continue down global middleware chain to subsequent listeners - * 2. augments args with step lifecycle-specific properties/utilities - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -// TODO :: refactor to incorporate a generic parameter -export function prepareStepArgs(args: AllWorkflowStepMiddlewareArgs): AllWorkflowStepMiddlewareArgs { - const { next: _next, ...stepArgs } = args; - // biome-ignore lint/suspicious/noExplicitAny: need to use any as the cases of the switch that follows dont narrow to the specific required args type. use type predicates for each workflow_step event args in the switch to get rid of this any. - const preparedArgs: any = { ...stepArgs }; - - switch (preparedArgs.payload.type) { - case 'workflow_step_edit': - preparedArgs.step = preparedArgs.action.workflow_step; - preparedArgs.configure = createStepConfigure(preparedArgs); - break; - case 'workflow_step': - preparedArgs.step = preparedArgs.body.workflow_step; - preparedArgs.update = createStepUpdate(preparedArgs); - break; - case 'workflow_step_execute': - preparedArgs.step = preparedArgs.event.workflow_step; - preparedArgs.complete = createStepComplete(preparedArgs); - preparedArgs.fail = createStepFail(preparedArgs); - break; - default: - break; - } - - return preparedArgs; -} diff --git a/src/errors.ts b/src/errors.ts index ef250b44c..40b1be79e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -42,9 +42,6 @@ export enum ErrorCode { */ UnknownError = 'slack_bolt_unknown_error', - // TODO: remove workflow step stuff in bolt v5 - WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error', - CustomFunctionInitializationError = 'slack_bolt_custom_function_initialization_error', CustomFunctionCompleteSuccessError = 'slack_bolt_custom_function_complete_success_error', CustomFunctionCompleteFailError = 'slack_bolt_custom_function_complete_fail_error', @@ -156,14 +153,6 @@ export class MultipleListenerError extends Error implements CodedError { this.originals = originals; } } -/** - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export class WorkflowStepInitializationError extends Error implements CodedError { - public code = ErrorCode.WorkflowStepInitializationError; -} - export class CustomFunctionInitializationError extends Error implements CodedError { public code = ErrorCode.CustomFunctionInitializationError; } diff --git a/src/helpers.ts b/src/helpers.ts index db6417c7a..a0ad1ca89 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -82,8 +82,7 @@ export function getTypeAndConversation(body: any): { type?: IncomingEventType; c conversationId: optionsBody.channel !== undefined ? optionsBody.channel.id : undefined, }; } - // TODO: remove workflow_step stuff in v5 - if (body.actions !== undefined || body.type === 'dialog_submission' || body.type === 'workflow_step_edit') { + if (body.actions !== undefined || body.type === 'dialog_submission') { const actionBody = body as SlackActionMiddlewareArgs['body']; return { type: IncomingEventType.Action, diff --git a/src/index.ts b/src/index.ts index 2f20aeef2..5dbf6fba3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -71,14 +71,6 @@ export { AssistantUserMessageMiddleware, } from './Assistant'; -export { - WorkflowStep, - WorkflowStepConfig, - WorkflowStepEditMiddleware, - WorkflowStepSaveMiddleware, - WorkflowStepExecuteMiddleware, -} from './WorkflowStep'; - // Re-export OAuth runtime classes export { MemoryInstallationStore, FileInstallationStore } from '@slack/oauth'; diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts index 9a9b120bf..fd38838cf 100644 --- a/src/types/actions/index.ts +++ b/src/types/actions/index.ts @@ -4,13 +4,9 @@ import type { AckFn, RespondFn, SayArguments, SayFn } from '../utilities'; import type { BlockAction } from './block-action'; import type { DialogSubmitAction, DialogValidation } from './dialog-action'; import type { InteractiveMessage } from './interactive-message'; -import type { WorkflowStepEdit } from './workflow-step-edit'; - export * from './block-action'; export * from './interactive-message'; export * from './dialog-action'; -// TODO: remove workflow step stuff in bolt v5 -export * from './workflow-step-edit'; /** * All known actions from Slack's Block Kit interactive components, message actions, dialogs, and legacy interactive @@ -26,8 +22,7 @@ export * from './workflow-step-edit'; * offered when no generic parameter is bound would be limited to BasicElementAction rather than the union of known * actions - ElementAction. */ -// TODO: remove workflow step stuff in bolt v5 -export type SlackAction = BlockAction | InteractiveMessage | DialogSubmitAction | WorkflowStepEdit; +export type SlackAction = BlockAction | InteractiveMessage | DialogSubmitAction; export interface ActionConstraints { type?: A['type']; @@ -66,9 +61,8 @@ export type SlackActionMiddlewareArgs complete?: FunctionCompleteFn; fail?: FunctionFailFn; inputs?: FunctionInputs; - // TODO: remove workflow step stuff in bolt v5 -} & (Action extends Exclude - ? // all action types except dialog submission and steps from apps have a channel context +} & (Action extends Exclude + ? // all action types except dialog submission have a channel context // TODO: not exactly true: a block action could occur from a view. should improve this. { say: SayFn } : unknown); diff --git a/src/types/actions/workflow-step-edit.ts b/src/types/actions/workflow-step-edit.ts deleted file mode 100644 index aa3c37bcd..000000000 --- a/src/types/actions/workflow-step-edit.ts +++ /dev/null @@ -1,54 +0,0 @@ -/** - * A Slack step from app action wrapped in the standard metadata. - * - * This describes the entire JSON-encoded body of a request from Slack step from app actions. - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface WorkflowStepEdit { - type: 'workflow_step_edit'; - callback_id: string; - trigger_id: string; - user: { - id: string; - username: string; - team_id?: string; // undocumented - }; - team: { - id: string; - domain: string; - enterprise_id?: string; // undocumented - enterprise_name?: string; // undocumented - }; - channel?: { - id?: string; - name?: string; - }; - token: string; - action_ts: string; // undocumented - workflow_step: { - workflow_id: string; - step_id: string; - inputs: Record< - string, - { - // biome-ignore lint/suspicious/noExplicitAny: input parameters can accept anything - value: any; - } - >; - outputs: { - name: string; - type: string; - label: string; - }[]; - step_name?: string; - step_image_url?: string; - }; - - // exists for enterprise installs - is_enterprise_install?: boolean; - enterprise?: { - id: string; - name: string; - }; -} diff --git a/src/types/view/index.ts b/src/types/view/index.ts index 585f0918c..ac5f1c87d 100644 --- a/src/types/view/index.ts +++ b/src/types/view/index.ts @@ -5,11 +5,7 @@ import type { AckFn, RespondFn } from '../utilities'; /** * Known view action types */ -export type SlackViewAction = - | ViewSubmitAction - | ViewClosedAction - | ViewWorkflowStepSubmitAction // TODO: remove workflow step stuff in bolt v5 - | ViewWorkflowStepClosedAction; +export type SlackViewAction = ViewSubmitAction | ViewClosedAction; // // TODO: add a type parameter here, just like the other constraint interfaces have. export interface ViewConstraints { @@ -104,38 +100,6 @@ export interface ViewClosedAction { }; } -/** - * A Slack view_submission step from app event - * - * This describes the additional JSON-encoded body details for a step's view_submission event - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface ViewWorkflowStepSubmitAction extends ViewSubmitAction { - trigger_id: string; - response_urls?: ViewResponseUrl[]; - workflow_step: { - workflow_step_edit_id: string; - workflow_id: string; - step_id: string; - }; -} - -/** - * A Slack view_closed step from app event - * - * This describes the additional JSON-encoded body details for a step's view_closed event - * @deprecated Steps from Apps are no longer supported and support for them will be removed in the next major bolt-js - * version. - */ -export interface ViewWorkflowStepClosedAction extends ViewClosedAction { - workflow_step: { - workflow_step_edit_id: string; - workflow_id: string; - step_id: string; - }; -} - export interface ViewStateSelectedOption { text: PlainTextElement; value: string; diff --git a/test/unit/WorkflowStep.spec.ts b/test/unit/WorkflowStep.spec.ts deleted file mode 100644 index dab9cc663..000000000 --- a/test/unit/WorkflowStep.spec.ts +++ /dev/null @@ -1,388 +0,0 @@ -import path from 'node:path'; -import type { WebClient } from '@slack/web-api'; -import { assert } from 'chai'; -import sinon from 'sinon'; -import { - type AllWorkflowStepMiddlewareArgs, - type SlackWorkflowStepMiddlewareArgs, - WorkflowStep, - type WorkflowStepConfig, - type WorkflowStepEditMiddlewareArgs, - type WorkflowStepExecuteMiddlewareArgs, - type WorkflowStepMiddleware, - type WorkflowStepSaveMiddlewareArgs, -} from '../../src/WorkflowStep'; -import { WorkflowStepInitializationError } from '../../src/errors'; -import type { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware, WorkflowStepEdit } from '../../src/types'; -import { type Override, noopVoid, proxyquire } from './helpers'; - -function importWorkflowStep(overrides: Override = {}): typeof import('../../src/WorkflowStep') { - const absolutePath = path.resolve(__dirname, '../../src/WorkflowStep'); - return proxyquire(absolutePath, overrides); -} - -const MOCK_CONFIG_SINGLE = { - edit: noopVoid, - save: noopVoid, - execute: noopVoid, -}; - -const MOCK_CONFIG_MULTIPLE = { - edit: [noopVoid, noopVoid], - save: [noopVoid], - execute: [noopVoid, noopVoid, noopVoid], -}; - -describe('WorkflowStep class', () => { - describe('constructor', () => { - it('should accept config as single functions', async () => { - const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_SINGLE); - assert.isNotNull(ws); - }); - - it('should accept config as multiple functions', async () => { - const ws = new WorkflowStep('test_callback_id', MOCK_CONFIG_MULTIPLE); - assert.isNotNull(ws); - }); - }); - - describe('getMiddleware', () => { - it('should not call next if a workflow step event', async () => { - const ws = new WorkflowStep('test_edit_callback_id', MOCK_CONFIG_SINGLE); - const middleware = ws.getMiddleware(); - const fakeEditArgs = createFakeStepEditAction() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - - const fakeNext = sinon.spy(); - fakeEditArgs.next = fakeNext; - - await middleware(fakeEditArgs); - - assert(fakeNext.notCalled); - }); - - it('should call next if valid workflow step with mismatched callback_id', async () => { - const ws = new WorkflowStep('bad_callback_id', MOCK_CONFIG_SINGLE); - const middleware = ws.getMiddleware(); - const fakeEditArgs = createFakeStepEditAction() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - - const fakeNext = sinon.spy(); - fakeEditArgs.next = fakeNext; - - await middleware(fakeEditArgs); - - assert(fakeNext.called); - }); - - it('should call next if not a workflow step event', async () => { - const ws = new WorkflowStep('test_view_callback_id', MOCK_CONFIG_SINGLE); - const middleware = ws.getMiddleware(); - const fakeViewArgs = createFakeViewEvent() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - - const fakeNext = sinon.spy(); - fakeViewArgs.next = fakeNext; - - await middleware(fakeViewArgs); - - assert(fakeNext.called); - }); - }); - - describe('validate', () => { - it('should throw an error if callback_id is not valid', async () => { - const { validate } = importWorkflowStep(); - - // intentionally casting to string to trigger failure - const badId = {} as string; - const validationFn = () => validate(badId, MOCK_CONFIG_SINGLE); - - const expectedMsg = 'WorkflowStep expects a callback_id as the first argument'; - assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); - }); - - it('should throw an error if config is not an object', async () => { - const { validate } = importWorkflowStep(); - - // intentionally casting to WorkflowStepConfig to trigger failure - const badConfig = '' as unknown as WorkflowStepConfig; - - const validationFn = () => validate('callback_id', badConfig); - const expectedMsg = 'WorkflowStep expects a configuration object as the second argument'; - assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); - }); - - it('should throw an error if required keys are missing', async () => { - const { validate } = importWorkflowStep(); - - // intentionally casting to WorkflowStepConfig to trigger failure - const badConfig = { - edit: async () => {}, - } as unknown as WorkflowStepConfig; - - const validationFn = () => validate('callback_id', badConfig); - const expectedMsg = 'WorkflowStep is missing required keys: save, execute'; - assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); - }); - - it('should throw an error if lifecycle props are not a single callback or an array of callbacks', async () => { - const { validate } = importWorkflowStep(); - - // intentionally casting to WorkflowStepConfig to trigger failure - const badConfig = { - edit: async () => {}, - save: {}, - execute: async () => {}, - } as unknown as WorkflowStepConfig; - - const validationFn = () => validate('callback_id', badConfig); - const expectedMsg = 'WorkflowStep save property must be a function or an array of functions'; - assert.throws(validationFn, WorkflowStepInitializationError, expectedMsg); - }); - }); - - describe('isStepEvent', () => { - it('should return true if recognized workflow step payload type', async () => { - const fakeEditArgs = createFakeStepEditAction() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - const fakeSaveArgs = createFakeStepSaveEvent() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - const fakeExecuteArgs = createFakeStepExecuteEvent() as unknown as SlackWorkflowStepMiddlewareArgs & - AllMiddlewareArgs; - - const { isStepEvent } = importWorkflowStep(); - - const editIsStepEvent = isStepEvent(fakeEditArgs); - const viewIsStepEvent = isStepEvent(fakeSaveArgs); - const executeIsStepEvent = isStepEvent(fakeExecuteArgs); - - assert.isTrue(editIsStepEvent); - assert.isTrue(viewIsStepEvent); - assert.isTrue(executeIsStepEvent); - }); - - it('should return false if not a recognized workflow step payload type', async () => { - const fakeEditArgs = createFakeStepEditAction() as unknown as AnyMiddlewareArgs; - fakeEditArgs.payload.type = 'invalid_type'; - - const { isStepEvent } = importWorkflowStep(); - const actionIsStepEvent = isStepEvent(fakeEditArgs); - - assert.isFalse(actionIsStepEvent); - }); - }); - - describe('prepareStepArgs', () => { - it('should remove next() from all original event args', async () => { - const fakeEditArgs = createFakeStepEditAction() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - const fakeSaveArgs = createFakeStepSaveEvent() as unknown as SlackWorkflowStepMiddlewareArgs & AllMiddlewareArgs; - const fakeExecuteArgs = createFakeStepExecuteEvent() as unknown as SlackWorkflowStepMiddlewareArgs & - AllMiddlewareArgs; - - const { prepareStepArgs } = importWorkflowStep(); - - const editStepArgs = prepareStepArgs(fakeEditArgs); - const viewStepArgs = prepareStepArgs(fakeSaveArgs); - const executeStepArgs = prepareStepArgs(fakeExecuteArgs); - - assert.notExists(editStepArgs.next); - assert.notExists(viewStepArgs.next); - assert.notExists(executeStepArgs.next); - }); - - it('should augment workflow_step_edit args with step and configure()', async () => { - const fakeArgs = createFakeStepEditAction(); - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const stepArgs = prepareStepArgs(fakeArgs as AllWorkflowStepMiddlewareArgs); - - assert.exists(stepArgs.step); - assert.property(stepArgs, 'configure'); - }); - - it('should augment view_submission with step and update()', async () => { - const fakeArgs = createFakeStepSaveEvent(); - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const stepArgs = prepareStepArgs( - fakeArgs as unknown as AllWorkflowStepMiddlewareArgs, - ); - - assert.exists(stepArgs.step); - assert.property(stepArgs, 'update'); - }); - - it('should augment workflow_step_execute with step, complete() and fail()', async () => { - const fakeArgs = createFakeStepExecuteEvent(); - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const stepArgs = prepareStepArgs( - fakeArgs as unknown as AllWorkflowStepMiddlewareArgs, - ); - - assert.exists(stepArgs.step); - assert.property(stepArgs, 'complete'); - assert.property(stepArgs, 'fail'); - }); - }); - - describe('step utility functions', () => { - it('configure should call views.open', async () => { - const fakeEditArgs = createFakeStepEditAction() as unknown as AllWorkflowStepMiddlewareArgs; - - const fakeClient = { views: { open: sinon.spy() } }; - fakeEditArgs.client = fakeClient as unknown as WebClient; - - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const editStepArgs = prepareStepArgs( - fakeEditArgs, - ) as AllWorkflowStepMiddlewareArgs; - - await editStepArgs.configure({ blocks: [] }); - - assert(fakeClient.views.open.called); - }); - - it('update should call workflows.updateStep', async () => { - const fakeSaveArgs = createFakeStepSaveEvent() as unknown as AllWorkflowStepMiddlewareArgs; - - const fakeClient = { workflows: { updateStep: sinon.spy() } }; - fakeSaveArgs.client = fakeClient as unknown as WebClient; - - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const saveStepArgs = prepareStepArgs( - fakeSaveArgs, - ) as AllWorkflowStepMiddlewareArgs; - - await saveStepArgs.update(); - - assert(fakeClient.workflows.updateStep.called); - }); - - it('complete should call workflows.stepCompleted', async () => { - const fakeExecuteArgs = createFakeStepExecuteEvent() as unknown as SlackWorkflowStepMiddlewareArgs & - AllMiddlewareArgs; // eslint-disable-line max-len - - const fakeClient = { workflows: { stepCompleted: sinon.spy() } }; - fakeExecuteArgs.client = fakeClient as unknown as WebClient; - - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const executeStepArgs = prepareStepArgs( - fakeExecuteArgs, - ) as AllWorkflowStepMiddlewareArgs; - - await executeStepArgs.complete(); - - assert(fakeClient.workflows.stepCompleted.called); - }); - - it('fail should call workflows.stepFailed', async () => { - const fakeExecuteArgs = createFakeStepExecuteEvent() as unknown as SlackWorkflowStepMiddlewareArgs & - AllMiddlewareArgs; // eslint-disable-line max-len - - const fakeClient = { workflows: { stepFailed: sinon.spy() } }; - fakeExecuteArgs.client = fakeClient as unknown as WebClient; - - const { prepareStepArgs } = importWorkflowStep(); - // casting to returned type because prepareStepArgs isn't built to do so - const executeStepArgs = prepareStepArgs( - fakeExecuteArgs, - ) as AllWorkflowStepMiddlewareArgs; - - await executeStepArgs.fail({ error: { message: 'Failed' } }); - - assert(fakeClient.workflows.stepFailed.called); - }); - }); - - describe('processStepMiddleware', () => { - it('should call each callback in user-provided middleware', async () => { - const { ...fakeArgs } = createFakeStepEditAction() as unknown as AllWorkflowStepMiddlewareArgs; - const { processStepMiddleware } = importWorkflowStep(); - - const fn1 = sinon.spy((async ({ next: continuation }) => { - await continuation(); - }) as Middleware); - const fn2 = sinon.spy(async () => {}); - const fakeMiddleware = [fn1, fn2] as WorkflowStepMiddleware; - - await processStepMiddleware(fakeArgs, fakeMiddleware); - - assert(fn1.called); - assert(fn2.called); - }); - }); -}); - -// TODO: need middleware test utilities like wrapping in AllMiddleWareArgs (creating say, respond, context) -// same for other kinds of middleware -// this stuff probably already exists -function createFakeStepEditAction() { - return { - body: { - callback_id: 'test_edit_callback_id', - trigger_id: 'test_edit_trigger_id', - }, - payload: { - type: 'workflow_step_edit', - callback_id: 'test_edit_callback_id', - }, - action: { - workflow_step: {}, - }, - context: {}, - }; -} - -function createFakeStepSaveEvent() { - return { - body: { - callback_id: 'test_save_callback_id', - trigger_id: 'test_save_trigger_id', - workflow_step: { - workflow_step_edit_id: '', - }, - }, - payload: { - type: 'workflow_step', - callback_id: 'test_save_callback_id', - }, - context: {}, - }; -} - -function createFakeStepExecuteEvent() { - return { - body: { - callback_id: 'test_execute_callback_id', - trigger_id: 'test_execute_trigger_id', - }, - event: { - workflow_step: {}, - }, - payload: { - type: 'workflow_step_execute', - callback_id: 'test_execute_callback_id', - workflow_step: { - workflow_step_execute_id: '', - }, - }, - context: {}, - }; -} - -function createFakeViewEvent() { - return { - body: { - callback_id: 'test_view_callback_id', - trigger_id: 'test_view_trigger_id', - workflow_step: { - workflow_step_edit_id: '', - }, - }, - payload: { - type: 'view_submission', - callback_id: 'test_view_callback_id', - }, - context: {}, - }; -} From a3b731c9b602d795aadf121c5d60f9fea0801ceb Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 19:39:11 -0400 Subject: [PATCH 03/11] feat: remove references to axios in favor of fetch (#2929) --- .changeset/use-native-fetch.md | 5 + package-lock.json | 252 +++++--------------- package.json | 11 +- src/App.ts | 32 +-- src/context/create-respond.ts | 15 +- src/receivers/SocketModeReceiver.ts | 4 + test/types/App.test-d.ts | 2 - test/unit/App/middlewares/arguments.spec.ts | 64 ++--- test/unit/context/create-respond.spec.ts | 29 ++- test/unit/helpers/app.ts | 10 - 10 files changed, 133 insertions(+), 291 deletions(-) create mode 100644 .changeset/use-native-fetch.md diff --git a/.changeset/use-native-fetch.md b/.changeset/use-native-fetch.md new file mode 100644 index 000000000..19f2dc9e0 --- /dev/null +++ b/.changeset/use-native-fetch.md @@ -0,0 +1,5 @@ +--- +"@slack/bolt": major +--- + +Replace axios with native fetch for response_url calls. Remove `agent` and `clientTls` options from `AppOptions` — use `clientOptions.fetch` to provide a custom fetch implementation for proxy/TLS needs. Add `dispatcher` option to `SocketModeReceiver` for proxy/TLS configuration in socket mode. diff --git a/package-lock.json b/package-lock.json index bfe15ac60..a44bd44f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,11 @@ "version": "4.7.2", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/oauth": "^3.0.5", - "@slack/socket-mode": "^2.0.7", - "@slack/types": "^2.21.1", - "@slack/web-api": "^7.15.2", - "axios": "^1.12.0", + "@slack/logger": "^5.0.0-rc.1", + "@slack/oauth": "^4.0.0-rc.1", + "@slack/socket-mode": "^3.0.0-rc.2", + "@slack/types": "^3.0.0-rc.1", + "@slack/web-api": "^8.0.0-rc.1", "express": "^5.0.0", "path-to-regexp": "^8.1.0", "raw-body": "^3", @@ -916,81 +915,82 @@ } }, "node_modules/@slack/logger": { - "version": "4.0.1", + "version": "5.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-5.0.0-rc.1.tgz", + "integrity": "sha512-3vO8zNGvk8n8tXpAzhIz1u/fHjhsLxGMhlZqzJEa3FxlXAe2lsY3qn8XBgKYEG2LmGP6ZWWmDq7vAPr2gZe2CQ==", "license": "MIT", "dependencies": { - "@types/node": ">=18" + "@types/node": ">=20" }, "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, "node_modules/@slack/oauth": { - "version": "3.0.5", + "version": "4.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@slack/oauth/-/oauth-4.0.0-rc.1.tgz", + "integrity": "sha512-ciE79zenceNBukfWjSoqxAf335wSRDsl0xU/TdK1i9j/5XtPSx2h23OPjWt6IPOVIypmdGZiwe62iX/p2ulHsQ==", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/web-api": "^7.15.0", + "@slack/logger": "^5.0.0-rc.1", + "@slack/web-api": "^8.0.0-rc.1", "@types/jsonwebtoken": "^9", - "@types/node": ">=18", + "@types/node": ">=20", "jsonwebtoken": "^9" }, "engines": { - "node": ">=18", - "npm": ">=8.6.0" + "node": ">=20", + "npm": ">=9.6.4" } }, "node_modules/@slack/socket-mode": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-2.0.7.tgz", - "integrity": "sha512-qYy07je71WnEHgRwmw12DlAnZLi5HXmdlI2WUzUK2LH/rYXQpP6uEg462S5CwfE8FoCKUdIigHtYnOOfzZH1lQ==", + "version": "3.0.0-rc.2", + "resolved": "https://registry.npmjs.org/@slack/socket-mode/-/socket-mode-3.0.0-rc.2.tgz", + "integrity": "sha512-otuxvm+fRdaUeJHxfQla4iDULbBRiKQrXjagSBFxJkjCpYCOPCdkIorc6LiM/GO9i7RWYRQZKzOj8fNv92PKyg==", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/web-api": "^7.15.0", - "@types/node": ">=18", - "@types/ws": "^8", - "eventemitter3": "^5", - "ws": "^8" + "@slack/logger": "^5.0.0-rc.1", + "@slack/web-api": "^8.0.0-rc.1", + "@types/node": ">=20", + "eventemitter3": "^5" }, "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "node": ">=20", + "npm": ">=9.6.4" + }, + "peerDependencies": { + "undici": "^7.0.0" } }, "node_modules/@slack/types": { - "version": "2.21.1", - "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.21.1.tgz", - "integrity": "sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ==", + "version": "3.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-3.0.0-rc.1.tgz", + "integrity": "sha512-xJZm26o5YK95OdM8BliTE+LijNhMGAwLWWpSpfnrPno4DyLBthNAjC7SG/9Ow2gB7oXCeYadpF/nzlYz7XaATg==", "license": "MIT", "engines": { - "node": ">= 12.13.0", - "npm": ">= 6.12.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, "node_modules/@slack/web-api": { - "version": "7.15.2", - "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.2.tgz", - "integrity": "sha512-/m9qVFkiq85Oa/FSQwYIRDa/AO4qNYkDh4sRBK1WqEc2+RyG7w4tbU6rBIwUOcc/TmWOIr24Nraquxg7um5mYw==", + "version": "8.0.0-rc.1", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-8.0.0-rc.1.tgz", + "integrity": "sha512-ZFJCYoAq0kC4pUe3V41YUOVvG/mceZ1yCcFMRCkYNu5joEuzkhKQpU4XfD2LnkJenWKwW6mg9J5KbBDMRCxCjg==", "license": "MIT", "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/types": "^2.21.0", - "@types/node": ">=18", + "@slack/logger": "^5.0.0-rc.1", + "@slack/types": "^3.0.0-rc.1", + "@types/node": ">=20", "@types/retry": "0.12.0", - "axios": "^1.15.0", "eventemitter3": "^5.0.1", - "form-data": "^4.0.4", - "is-electron": "2.2.2", - "is-stream": "^2", "p-queue": "^6", "p-retry": "^4", "retry": "^0.13.1" }, "engines": { - "node": ">= 18", - "npm": ">= 8.6.0" + "node": ">= 20", + "npm": ">=9.6.4" } }, "node_modules/@tsconfig/node10": { @@ -1196,13 +1196,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/accepts": { "version": "2.0.0", "license": "MIT", @@ -1326,21 +1319,6 @@ "node": "*" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, - "node_modules/axios": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz", - "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==", - "license": "MIT", - "dependencies": { - "follow-redirects": "^1.15.11", - "form-data": "^4.0.5", - "proxy-from-env": "^2.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "dev": true, @@ -1611,16 +1589,6 @@ "dev": true, "license": "MIT" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "license": "MIT", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/concat-map": { "version": "0.0.1", "dev": true, @@ -1738,13 +1706,6 @@ "node": ">=6" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/depd": { "version": "2.0.0", "license": "MIT", @@ -1871,19 +1832,6 @@ "node": ">= 0.4" } }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "dev": true, @@ -2098,24 +2046,6 @@ "flat": "cli.js" } }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.3.1", "dev": true, @@ -2131,37 +2061,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/form-data": { - "version": "4.0.5", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/form-data/node_modules/mime-db": { - "version": "1.52.0", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/form-data/node_modules/mime-types": { - "version": "2.1.35", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/forwarded": { "version": "0.2.0", "license": "MIT", @@ -2351,19 +2250,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -2520,10 +2406,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-electron": { - "version": "2.2.2", - "license": "MIT" - }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -2579,16 +2461,6 @@ "version": "4.0.0", "license": "MIT" }, - "node_modules/is-stream": { - "version": "2.0.1", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-subdir": { "version": "1.2.0", "dev": true, @@ -3499,13 +3371,6 @@ "node": ">= 0.10" } }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/proxyquire": { "version": "2.1.3", "dev": true, @@ -4573,6 +4438,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", + "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/universalify": { "version": "0.1.2", "dev": true, @@ -4687,25 +4562,6 @@ "version": "1.0.2", "license": "ISC" }, - "node_modules/ws": { - "version": "8.20.0", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/y18n": { "version": "5.0.8", "dev": true, diff --git a/package.json b/package.json index d0cf251be..09807b0c0 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,11 @@ "url": "https://github.com/slackapi/bolt-js/issues" }, "dependencies": { - "@slack/logger": "^4.0.1", - "@slack/oauth": "^3.0.5", - "@slack/socket-mode": "^2.0.7", - "@slack/types": "^2.21.1", - "@slack/web-api": "^7.15.2", - "axios": "^1.12.0", + "@slack/logger": "^5.0.0-rc.1", + "@slack/oauth": "^4.0.0-rc.1", + "@slack/socket-mode": "^3.0.0-rc.2", + "@slack/types": "^3.0.0-rc.1", + "@slack/web-api": "^8.0.0-rc.1", "express": "^5.0.0", "path-to-regexp": "^8.1.0", "raw-body": "^3", diff --git a/src/App.ts b/src/App.ts index 56ba1538b..00a097aff 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,9 +1,6 @@ -import type { Agent } from 'node:http'; -import type { SecureContextOptions } from 'node:tls'; import util from 'node:util'; import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; -import { WebClient, type WebClientOptions, addAppMetadata } from '@slack/web-api'; -import axios, { type AxiosInstance } from 'axios'; +import { type FetchFunction, WebClient, type WebClientOptions, addAppMetadata } from '@slack/web-api'; import type { Assistant } from './Assistant'; import { CustomFunction, @@ -126,8 +123,6 @@ export interface AppOptions { installationStore?: HTTPReceiverOptions['installationStore']; // default MemoryInstallationStore scopes?: HTTPReceiverOptions['scopes']; installerOptions?: HTTPReceiverOptions['installerOptions']; - agent?: Agent; - clientTls?: Pick; convoStore?: ConversationStore | false; token?: AuthorizeResult['botToken']; // either token or authorize appToken?: string; // TODO should this be included in AuthorizeResult @@ -254,7 +249,7 @@ export default class App private errorHandler: AnyErrorHandler; - private axios: AxiosInstance; + private fetchFn: FetchFunction; private installerOptions: HTTPReceiverOptions['installerOptions']; @@ -286,8 +281,6 @@ export default class App endpoints = undefined, port = undefined, customRoutes = undefined, - agent = undefined, - clientTls = undefined, receiver = undefined, convoStore = undefined, token = undefined, @@ -351,12 +344,6 @@ export default class App /* ------------------------ Set client options ------------------------*/ this.clientOptions = clientOptions !== undefined ? clientOptions : {}; - if (agent !== undefined && this.clientOptions.agent === undefined) { - this.clientOptions.agent = agent; - } - if (clientTls !== undefined && this.clientOptions.tls === undefined) { - this.clientOptions.tls = clientTls; - } if (logLevel !== undefined && logger === undefined) { // only logLevel is passed this.clientOptions.logLevel = logLevel; @@ -368,16 +355,7 @@ export default class App // Since v3.4, it can have the passed token in the case of single workspace installation. this.client = new WebClient(token, this.clientOptions); - this.axios = axios.create({ - httpAgent: agent, - httpsAgent: agent, - // disabling axios' automatic proxy support: - // axios would read from env vars to configure a proxy automatically, but it doesn't support TLS destinations. - // for compatibility with https://api.slack.com, and for a larger set of possible proxies (SOCKS or other - // protocols), users of this package should use the `agent` option to configure a proxy. - proxy: false, - ...clientTls, - }); + this.fetchFn = this.clientOptions.fetch ?? globalThis.fetch; this.middleware = []; this.listeners = []; @@ -1147,10 +1125,10 @@ export default class App // Set respond() utility if (body.response_url) { - listenerArgs.respond = createRespond(this.axios, body.response_url); + listenerArgs.respond = createRespond(this.fetchFn, body.response_url); } else if (typeof body.response_urls !== 'undefined' && body.response_urls.length > 0) { // This can exist only when view_submission payloads - response_url_enabled: true - listenerArgs.respond = createRespond(this.axios, body.response_urls[0].response_url); + listenerArgs.respond = createRespond(this.fetchFn, body.response_urls[0].response_url); } // Set ack() utility diff --git a/src/context/create-respond.ts b/src/context/create-respond.ts index 8daf71987..ca2a81b87 100644 --- a/src/context/create-respond.ts +++ b/src/context/create-respond.ts @@ -1,12 +1,13 @@ -import type { AxiosInstance, AxiosResponse } from 'axios'; -import type { RespondArguments } from '../types'; +import type { FetchFunction } from '@slack/web-api'; +import type { RespondArguments, RespondFn } from '../types'; -export function createRespond( - axiosInstance: AxiosInstance, - responseUrl: string, -): (response: string | RespondArguments) => Promise { +export function createRespond(fetchFn: FetchFunction, responseUrl: string): RespondFn { return async (message: string | RespondArguments) => { const normalizedArgs: RespondArguments = typeof message === 'string' ? { text: message } : message; - return axiosInstance.post(responseUrl, normalizedArgs); + return fetchFn(responseUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(normalizedArgs), + }); }; } diff --git a/src/receivers/SocketModeReceiver.ts b/src/receivers/SocketModeReceiver.ts index de462095e..ca6b52c2e 100644 --- a/src/receivers/SocketModeReceiver.ts +++ b/src/receivers/SocketModeReceiver.ts @@ -9,6 +9,7 @@ import { type InstallURLOptions, } from '@slack/oauth'; import { SocketModeClient } from '@slack/socket-mode'; +import type { SocketModeOptions } from '@slack/socket-mode'; import type { AppsConnectionsOpenResponse } from '@slack/web-api'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; @@ -38,6 +39,7 @@ export interface SocketModeReceiverOptions { scopes?: InstallURLOptions['scopes']; installerOptions?: InstallerOptions; appToken: string; // App Level Token + dispatcher?: SocketModeOptions['dispatcher']; customRoutes?: CustomRoute[]; clientPingTimeout?: number; serverPingTimeout?: number; @@ -98,6 +100,7 @@ export default class SocketModeReceiver implements Receiver { public constructor({ appToken, + dispatcher, logger = undefined, logLevel = LogLevel.INFO, clientPingTimeout = undefined, @@ -117,6 +120,7 @@ export default class SocketModeReceiver implements Receiver { }: SocketModeReceiverOptions) { this.client = new SocketModeClient({ appToken, + dispatcher, logLevel, logger, clientPingTimeout, diff --git a/test/types/App.test-d.ts b/test/types/App.test-d.ts index 3c5b8071c..2aad64270 100644 --- a/test/types/App.test-d.ts +++ b/test/types/App.test-d.ts @@ -1,4 +1,3 @@ -import { Agent } from 'node:http'; import { ConsoleLogger, LogLevel } from '@slack/logger'; import type { Installation, InstallationQuery } from '@slack/oauth'; import { expectAssignable, expectError, expectType } from 'tsd'; @@ -50,7 +49,6 @@ expectAssignable( expectAssignable( new App({ clientOptions: { - agent: new Agent(), allowAbsoluteUrls: false, logger: new ConsoleLogger(), retryConfig: { diff --git a/test/unit/App/middlewares/arguments.spec.ts b/test/unit/App/middlewares/arguments.spec.ts index d28b16d65..43c6ca8e1 100644 --- a/test/unit/App/middlewares/arguments.spec.ts +++ b/test/unit/App/middlewares/arguments.spec.ts @@ -19,7 +19,6 @@ import { mergeOverrides, noop, noopMiddleware, - withAxiosPost, withChatStream, withConversationContext, withMemoryStore, @@ -59,8 +58,7 @@ describe('App middleware and listener arguments', () => { describe('authorize', () => { it('should extract valid enterprise_id in a shared channel #935', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const fakeHandler = sinon.fake(); @@ -96,8 +94,7 @@ describe('App middleware and listener arguments', () => { sinon.assert.calledOnce(fakeHandler); }); it('should be skipped for tokens_revoked events #674', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const fakeAuthorize = sinon.fake.resolves({}); const fakeHandler = sinon.fake(); @@ -137,8 +134,7 @@ describe('App middleware and listener arguments', () => { sinon.assert.calledOnce(fakeHandler); }); it('should be skipped for app_uninstalled events #674', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const fakeAuthorize = sinon.fake.resolves({}); const fakeHandler = sinon.fake(); @@ -180,11 +176,15 @@ describe('App middleware and listener arguments', () => { const responseText = 'response'; const response_url = 'https://fake.slack/response_url'; const action_id = 'block_action_id'; - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); - const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); + const app = new MockApp({ + receiver: fakeReceiver, + authorize: sinon.fake.resolves(dummyAuthorizationResult), + clientOptions: { fetch: fakeFetch }, + }); app.action(action_id, async ({ respond }) => { await respond(responseText); }); @@ -207,19 +207,24 @@ describe('App middleware and listener arguments', () => { ); sinon.assert.notCalled(fakeErrorHandler); - // Assert that each call to fakeAxiosPost had the right arguments - sinon.assert.calledOnceWithExactly(fakeAxiosPost, response_url, { text: responseText }); + sinon.assert.calledOnce(fakeFetch); + assert.equal(fakeFetch.firstCall.args[0], response_url); + assert.deepEqual(JSON.parse(fakeFetch.firstCall.args[1].body), { text: responseText }); }); it('should respond with a response object', async () => { const responseObject = { text: 'response' }; const response_url = 'https://fake.slack/response_url'; const action_id = 'block_action_id'; - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); - const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); + const app = new MockApp({ + receiver: fakeReceiver, + authorize: sinon.fake.resolves(dummyAuthorizationResult), + clientOptions: { fetch: fakeFetch }, + }); app.action(action_id, async ({ respond }) => { await respond(responseObject); }); @@ -241,17 +246,22 @@ describe('App middleware and listener arguments', () => { ), ); - // Assert that each call to fakeAxiosPost had the right arguments - sinon.assert.calledOnceWithExactly(fakeAxiosPost, response_url, responseObject); + sinon.assert.calledOnce(fakeFetch); + assert.equal(fakeFetch.firstCall.args[0], response_url); + assert.deepEqual(JSON.parse(fakeFetch.firstCall.args[1].body), responseObject); }); it('should be able to use respond for view_submission payloads', async () => { const responseObject = { text: 'response' }; const responseUrl = 'https://fake.slack/response_url'; - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); - const app = new MockApp({ receiver: fakeReceiver, authorize: sinon.fake.resolves(dummyAuthorizationResult) }); + const app = new MockApp({ + receiver: fakeReceiver, + authorize: sinon.fake.resolves(dummyAuthorizationResult), + clientOptions: { fetch: fakeFetch }, + }); app.view('view-id', async ({ respond }) => { await respond(responseObject); }); @@ -276,8 +286,9 @@ describe('App middleware and listener arguments', () => { ), ); - // Assert that each call to fakeAxiosPost had the right arguments - sinon.assert.calledOnceWithExactly(fakeAxiosPost, responseUrl, responseObject); + sinon.assert.calledOnce(fakeFetch); + assert.equal(fakeFetch.firstCall.args[0], responseUrl); + assert.deepEqual(JSON.parse(fakeFetch.firstCall.args[1].body), responseObject); }); }); @@ -891,8 +902,7 @@ describe('App middleware and listener arguments', () => { describe('context', () => { it('should be able to use the app_installed_team_id when provided by the payload', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const callback_id = 'view-id'; const app_installed_team_id = 'T-installed-workspace'; @@ -921,8 +931,7 @@ describe('App middleware and listener arguments', () => { }); it('should have function executed event details from a custom step payload', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const callbackId = 'reverse_string'; const functionBotAccessToken = 'xwfp-example'; @@ -963,8 +972,7 @@ describe('App middleware and listener arguments', () => { }); it('should have function executed event details from a block actions payload', async () => { - const fakeAxiosPost = sinon.fake.resolves({}); - overrides = buildOverrides([withNoopWebClient(), withAxiosPost(fakeAxiosPost)]); + overrides = buildOverrides([withNoopWebClient()]); const MockApp = importApp(overrides); const callbackId = 'reverse_string_button'; const functionBotAccessToken = 'xwfp-example'; diff --git a/test/unit/context/create-respond.spec.ts b/test/unit/context/create-respond.spec.ts index a24a37e57..fe02d1259 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -1,39 +1,42 @@ -import type { AxiosInstance } from 'axios'; +import type { FetchFunction } from '@slack/web-api'; import { assert } from 'chai'; import sinon from 'sinon'; import { createRespond } from '../../../src/context'; describe('createRespond', () => { it('should post to the response URL with text when given a string', async () => { - const axiosInstance = { post: sinon.stub().resolves({ status: 200 }) }; - const respond = createRespond(axiosInstance as unknown as AxiosInstance, 'https://hooks.slack.com/response/123'); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + const respond = createRespond(fakeFetch as unknown as FetchFunction, 'https://hooks.slack.com/response/123'); await respond('hello'); - assert(axiosInstance.post.calledOnce); - assert.equal(axiosInstance.post.firstCall.args[0], 'https://hooks.slack.com/response/123'); - assert.deepEqual(axiosInstance.post.firstCall.args[1], { text: 'hello' }); + assert(fakeFetch.calledOnce); + assert.equal(fakeFetch.firstCall.args[0], 'https://hooks.slack.com/response/123'); + assert.equal(fakeFetch.firstCall.args[1].method, 'POST'); + assert.deepEqual(JSON.parse(fakeFetch.firstCall.args[1].body), { text: 'hello' }); }); it('should post to the response URL with the full message object', async () => { const url = 'https://hooks.slack.com/response/123'; - const axiosInstance = { post: sinon.stub().resolves({ status: 200 }) }; - const respond = createRespond(axiosInstance as unknown as AxiosInstance, url); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + const respond = createRespond(fakeFetch as unknown as FetchFunction, url); const message = { text: 'hello', replace_original: true }; await respond(message); - assert(axiosInstance.post.calledOnceWithExactly(url, message)); + assert(fakeFetch.calledOnce); + assert.equal(fakeFetch.firstCall.args[0], url); + assert.deepEqual(JSON.parse(fakeFetch.firstCall.args[1].body), message); }); it('should use the correct response URL', async () => { const url = 'https://hooks.slack.com/response/456'; - const axiosInstance = { post: sinon.stub().resolves({ status: 200 }) }; - const respond = createRespond(axiosInstance as unknown as AxiosInstance, url); + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 200 })); + const respond = createRespond(fakeFetch as unknown as FetchFunction, url); await respond('test'); - assert(axiosInstance.post.calledOnce); - assert.equal(axiosInstance.post.firstCall.args[0], url); + assert(fakeFetch.calledOnce); + assert.equal(fakeFetch.firstCall.args[0], url); }); }); diff --git a/test/unit/helpers/app.ts b/test/unit/helpers/app.ts index 11aa076e3..0883de9db 100644 --- a/test/unit/helpers/app.ts +++ b/test/unit/helpers/app.ts @@ -131,16 +131,6 @@ export function withSetStatus(spy: SinonSpy): Override { }; } -export function withAxiosPost(spy: SinonSpy): Override { - return { - axios: { - create: () => ({ - post: spy, - }), - }, - }; -} - export function withSuccessfulBotUserFetchingWebClient(botId: string, botUserId: string): Override { return { '@slack/web-api': { From 709451192c551d2d6b3aec20bda6af50271c73d2 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 19:54:12 -0400 Subject: [PATCH 04/11] feat: improve error handling (#2930) --- .changeset/improve-error-handling.md | 5 ++++ src/App.ts | 29 ++++++++++++++++++------ src/receivers/HTTPModuleFunctions.ts | 26 +++++++++------------ src/receivers/SocketModeFunctions.ts | 4 ++-- test/unit/App/middlewares/global.spec.ts | 4 ++-- 5 files changed, 42 insertions(+), 26 deletions(-) create mode 100644 .changeset/improve-error-handling.md diff --git a/.changeset/improve-error-handling.md b/.changeset/improve-error-handling.md new file mode 100644 index 000000000..f7e3bb2ac --- /dev/null +++ b/.changeset/improve-error-handling.md @@ -0,0 +1,5 @@ +--- +"@slack/bolt": minor +--- + +Improve error handling by leveraging `@slack/web-api` v8 error classes. Authorization errors are now properly wrapped (preserving the original error's class identity). Default error handlers log richer details for web-api errors (API error codes, rate limit durations, HTTP status codes). Re-export `SlackError`, `WebAPIPlatformError`, `WebAPIRequestError`, `WebAPIHTTPError`, and `WebAPIRateLimitedError` from the package entry point. diff --git a/src/App.ts b/src/App.ts index 00a097aff..4273ddf9e 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,6 +1,14 @@ import util from 'node:util'; import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; -import { type FetchFunction, WebClient, type WebClientOptions, addAppMetadata } from '@slack/web-api'; +import { + type FetchFunction, + WebAPIHTTPError, + WebAPIPlatformError, + WebAPIRateLimitedError, + WebClient, + type WebClientOptions, + addAppMetadata, +} from '@slack/web-api'; import type { Assistant } from './Assistant'; import { CustomFunction, @@ -20,8 +28,8 @@ import type { SayStreamFn, SetStatusFn } from './context'; import { type ConversationStore, MemoryStore, conversationContext } from './conversation-store'; import { AppInitializationError, + AuthorizationError, type CodedError, - ErrorCode, InvalidCustomPropertyError, MultipleListenerError, asCodedError, @@ -918,12 +926,11 @@ export default class App try { authorizeResult = await this.authorize(source, bodyArg); } catch (error) { - // biome-ignore lint/suspicious/noExplicitAny: errors can be anything - const e = error as any; + const e = error instanceof Error ? error : new Error(String(error)); this.logger.warn('Authorization of incoming event did not succeed. No listeners will be called.'); - e.code = ErrorCode.AuthorizationError; + const authError = new AuthorizationError(`Authorization of incoming event did not succeed. ${e.message}`, e); await this.handleError({ - error: e, + error: authError, logger: this.logger, body: bodyArg, context: { @@ -1351,7 +1358,15 @@ export default class App function defaultErrorHandler(logger: Logger): ErrorHandler { return (error: CodedError) => { - logger.error(error); + if (error instanceof WebAPIPlatformError) { + logger.error(`Slack API error: ${error.data.error}`); + } else if (error instanceof WebAPIRateLimitedError) { + logger.error(`Rate limited, retry after ${error.retryAfter}s`); + } else if (error instanceof WebAPIHTTPError) { + logger.error(`HTTP error ${error.statusCode}: ${error.statusMessage}`); + } else { + logger.error(error); + } return Promise.reject(error); }; diff --git a/src/receivers/HTTPModuleFunctions.ts b/src/receivers/HTTPModuleFunctions.ts index 7091b71fd..239bb7c67 100644 --- a/src/receivers/HTTPModuleFunctions.ts +++ b/src/receivers/HTTPModuleFunctions.ts @@ -2,7 +2,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http'; import { parse as qsParse } from 'node:querystring'; import type { Logger } from '@slack/logger'; import rawBody from 'raw-body'; -import { type CodedError, ErrorCode } from '../errors'; +import { AuthorizationError, type CodedError, HTTPReceiverDeferredRequestError } from '../errors'; import type { BufferedIncomingMessage } from './BufferedIncomingMessage'; import { verifySlackRequest } from './verify-request'; @@ -166,13 +166,11 @@ export const buildContentResponse = (res: ServerResponse, body: any): void => { // Note that it was not possible to make this function async due to the limitation of http module export const defaultDispatchErrorHandler = (args: ReceiverDispatchErrorHandlerArgs): void => { const { error, logger, request, response } = args; - if ('code' in error) { - if (error.code === ErrorCode.HTTPReceiverDeferredRequestError) { - logger.info(`Unhandled HTTP request (${request.method}) made to ${request.url}`); - response.writeHead(404); - response.end(); - return; - } + if (error instanceof HTTPReceiverDeferredRequestError) { + logger.info(`Unhandled HTTP request (${request.method}) made to ${request.url}`); + response.writeHead(404); + response.end(); + return; } logger.error(`An unexpected error occurred during a request (${request.method}) made to ${request.url}`); logger.debug(`Error details: ${error}`); @@ -196,13 +194,11 @@ export const defaultProcessEventErrorHandler = async (args: ReceiverProcessEvent return false; } - if ('code' in error) { - if (error.code === ErrorCode.AuthorizationError) { - // authorize function threw an exception, which means there is no valid installation data - response.writeHead(401); - response.end(); - return true; - } + if (error instanceof AuthorizationError) { + // authorize function threw an exception, which means there is no valid installation data + response.writeHead(401); + response.end(); + return true; } logger.error('An unhandled error occurred while Bolt processed an event'); logger.debug(`Error details: ${error}, storedResponse: ${storedResponse}`); diff --git a/src/receivers/SocketModeFunctions.ts b/src/receivers/SocketModeFunctions.ts index ea8fc16fe..69b8043a1 100644 --- a/src/receivers/SocketModeFunctions.ts +++ b/src/receivers/SocketModeFunctions.ts @@ -1,5 +1,5 @@ import type { Logger } from '@slack/logger'; -import { type CodedError, ErrorCode, isCodedError } from '../errors'; +import { AuthorizationError, type CodedError } from '../errors'; import type { ReceiverEvent } from '../types'; export async function defaultProcessEventErrorHandler( @@ -11,7 +11,7 @@ export async function defaultProcessEventErrorHandler( // to return more properties to 'slack_event' listeners logger.error(`An unhandled error occurred while Bolt processed (type: ${event.body?.type}, error: ${error})`); logger.debug(`Error details: ${error}, retry num: ${event.retryNum}, retry reason: ${event.retryReason}`); - if (isCodedError(error) && error.code === ErrorCode.AuthorizationError) { + if (error instanceof AuthorizationError) { // The `authorize` function threw an exception, which means there is no valid installation data. // In this case, we can tell the Slack server-side to stop retries. return true; diff --git a/test/unit/App/middlewares/global.spec.ts b/test/unit/App/middlewares/global.spec.ts index 1f332e971..0f834a5fe 100644 --- a/test/unit/App/middlewares/global.spec.ts +++ b/test/unit/App/middlewares/global.spec.ts @@ -103,9 +103,9 @@ describe('App global middleware Processing', () => { assert(fakeMiddleware.notCalled); assert(fakeLogger.warn.called); - assert.instanceOf(fakeErrorHandler.firstCall.args[0], Error); + assert.instanceOf(fakeErrorHandler.firstCall.args[0], AuthorizationError); assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'code', ErrorCode.AuthorizationError); - assert.propertyVal(fakeErrorHandler.firstCall.args[0], 'original', dummyAuthorizationError.original); + assert.strictEqual(fakeErrorHandler.firstCall.args[0].original, dummyAuthorizationError); assert(fakeAck.called); }); From f63cfad28aaf38f5de15aca6442a37550356b131 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 19:55:21 -0400 Subject: [PATCH 05/11] chore: bump version for release candidate --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09807b0c0..0c689ad6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@slack/bolt", - "version": "4.7.2", + "version": "5.0.0-rc.1", "description": "A framework for building Slack apps, fast.", "author": "Slack Technologies, LLC", "license": "MIT", From b50019c06dba2dc4afd77dc1f4dc4f8df15c55f2 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Fri, 15 May 2026 00:38:08 -0700 Subject: [PATCH 06/11] chore: update biome configurations and applied settings (#2932) --- biome.json | 25 ++++- .../custom-receiver/src/FastifyReceiver.ts | 11 +-- examples/custom-receiver/src/KoaReceiver.ts | 13 +-- package-lock.json | 73 ++++++++------- package.json | 2 +- src/App.ts | 39 ++++---- src/Assistant.ts | 4 +- src/context/index.ts | 10 +- src/index.ts | 92 +++++++++---------- src/receivers/AwsLambdaReceiver.ts | 2 +- src/receivers/ExpressReceiver.ts | 13 ++- src/receivers/HTTPReceiver.ts | 8 +- src/receivers/SocketModeReceiver.ts | 10 +- src/types/actions/index.ts | 3 +- src/types/events/index.ts | 5 +- src/types/index.ts | 6 +- src/types/options/index.ts | 5 +- test/unit/App/basic.spec.ts | 2 +- test/unit/App/middlewares/arguments.spec.ts | 4 +- test/unit/App/middlewares/global.spec.ts | 2 +- test/unit/App/middlewares/ignore-self.spec.ts | 4 +- test/unit/App/middlewares/listener.spec.ts | 2 +- test/unit/App/routing-action.spec.ts | 6 +- test/unit/App/routing-assistant.spec.ts | 4 +- test/unit/App/routing-command.spec.ts | 4 +- test/unit/App/routing-event.spec.ts | 4 +- test/unit/App/routing-function.spec.ts | 4 +- test/unit/App/routing-message.spec.ts | 4 +- test/unit/App/routing-options.spec.ts | 4 +- test/unit/App/routing-shortcut.spec.ts | 6 +- test/unit/App/routing-view.spec.ts | 6 +- test/unit/Assistant.spec.ts | 2 +- test/unit/CustomFunction.spec.ts | 2 +- test/unit/conversation-store.spec.ts | 4 +- test/unit/errors.spec.ts | 2 +- test/unit/helpers.spec.ts | 2 +- test/unit/helpers/events.ts | 2 +- test/unit/helpers/index.ts | 2 +- test/unit/middleware/builtin.spec.ts | 4 +- test/unit/receivers/AwsLambdaReceiver.spec.ts | 2 +- test/unit/receivers/ExpressReceiver.spec.ts | 6 +- test/unit/receivers/HTTPReceiver.spec.ts | 4 +- .../unit/receivers/SocketModeReceiver.spec.ts | 4 +- 43 files changed, 205 insertions(+), 208 deletions(-) diff --git a/biome.json b/biome.json index dd06abddb..055e48274 100644 --- a/biome.json +++ b/biome.json @@ -1,12 +1,12 @@ { "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "files": { - "ignore": ["examples/**/dist"] + "includes": ["**", "!**/examples/**/dist"] }, "formatter": { "enabled": true, "formatWithErrors": false, - "ignore": [], + "includes": ["**"], "attributePosition": "auto", "indentStyle": "space", "indentWidth": 2, @@ -24,9 +24,26 @@ "recommended": true } }, - "organizeImports": { - "enabled": true + "assist": { + "actions": { + "source": { + "organizeImports": "on" + } + } }, + "overrides": [ + { + "includes": ["examples/**"], + "linter": { + "rules": { + "correctness": { + "noUnusedFunctionParameters": "off", + "noUnusedVariables": "off" + } + } + } + } + ], "vcs": { "enabled": true, "clientKind": "git", diff --git a/examples/custom-receiver/src/FastifyReceiver.ts b/examples/custom-receiver/src/FastifyReceiver.ts index 4ae2e647e..9ccc0102d 100644 --- a/examples/custom-receiver/src/FastifyReceiver.ts +++ b/examples/custom-receiver/src/FastifyReceiver.ts @@ -4,6 +4,7 @@ import { type BufferedIncomingMessage, type CodedError, HTTPResponseAck, + HTTPModuleFunctions as httpFunc, type InstallProviderOptions, type InstallURLOptions, type Receiver, @@ -11,9 +12,8 @@ import { ReceiverInconsistentStateError, type ReceiverProcessEventErrorHandlerArgs, type ReceiverUnhandledRequestHandlerArgs, - HTTPModuleFunctions as httpFunc, } from '@slack/bolt'; -import { ConsoleLogger, type LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, type LogLevel } from '@slack/logger'; import { type CallbackOptions, type InstallPathOptions, InstallProvider } from '@slack/oauth'; import Fastify, { type FastifyInstance } from 'fastify'; @@ -163,12 +163,7 @@ export default class FastifyReceiver implements Receiver { public init(app: App): void { this.app = app; - if ( - this.installer && - this.installerOptions && - this.installerOptions.installPath && - this.installerOptions.redirectUriPath - ) { + if (this.installer && this.installerOptions?.installPath && this.installerOptions.redirectUriPath) { this.fastify.get(this.installerOptions.installPath, async (req, res) => { await this.installer?.handleInstallPath(req.raw, res.raw, this.installerOptions?.installPathOptions); }); diff --git a/examples/custom-receiver/src/KoaReceiver.ts b/examples/custom-receiver/src/KoaReceiver.ts index 5dfeee8ef..faccd0ff2 100644 --- a/examples/custom-receiver/src/KoaReceiver.ts +++ b/examples/custom-receiver/src/KoaReceiver.ts @@ -1,10 +1,11 @@ -import { type Server, createServer } from 'node:http'; +import { createServer, type Server } from 'node:http'; import Router from '@koa/router'; import { type App, type BufferedIncomingMessage, type CodedError, HTTPResponseAck, + HTTPModuleFunctions as httpFunc, type InstallProviderOptions, type InstallURLOptions, type Receiver, @@ -12,9 +13,8 @@ import { ReceiverInconsistentStateError, type ReceiverProcessEventErrorHandlerArgs, type ReceiverUnhandledRequestHandlerArgs, - HTTPModuleFunctions as httpFunc, } from '@slack/bolt'; -import { ConsoleLogger, type LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, type LogLevel } from '@slack/logger'; import { type CallbackOptions, type InstallPathOptions, InstallProvider } from '@slack/oauth'; import Koa from 'koa'; @@ -158,12 +158,7 @@ export default class KoaReceiver implements Receiver { public init(app: App): void { this.app = app; - if ( - this.installer && - this.installerOptions && - this.installerOptions.installPath && - this.installerOptions.redirectUriPath - ) { + if (this.installer && this.installerOptions?.installPath && this.installerOptions.redirectUriPath) { this.router.get(this.installerOptions.installPath, async (ctx) => { await this.installer?.handleInstallPath(ctx.req, ctx.res, this.installerOptions?.installPathOptions); }); diff --git a/package-lock.json b/package-lock.json index a44bd44f4..35bc4640a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@slack/bolt", - "version": "4.7.2", + "version": "5.0.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@slack/bolt", - "version": "4.7.2", + "version": "5.0.0-rc.1", "license": "MIT", "dependencies": { "@slack/logger": "^5.0.0-rc.1", @@ -20,7 +20,7 @@ "tsscmp": "^1.0.6" }, "devDependencies": { - "@biomejs/biome": "^1.9.0", + "@biomejs/biome": "^2.4.15", "@changesets/cli": "^2.29.8", "@tsconfig/node20": "^20.1.5", "@types/chai": "^4.1.7", @@ -199,9 +199,10 @@ } }, "node_modules/@biomejs/biome": { - "version": "1.9.4", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.4.15.tgz", + "integrity": "sha512-j5VH3a/h/HXTKBM50MDMxRCzkeLv9S2XJcW2WgnZT1+xyisi+0bISrXR82gCX+8S9lvK0skEvHJRN+3Ktr2hlw==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -214,18 +215,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.4.15", + "@biomejs/cli-darwin-x64": "2.4.15", + "@biomejs/cli-linux-arm64": "2.4.15", + "@biomejs/cli-linux-arm64-musl": "2.4.15", + "@biomejs/cli-linux-x64": "2.4.15", + "@biomejs/cli-linux-x64-musl": "2.4.15", + "@biomejs/cli-win32-arm64": "2.4.15", + "@biomejs/cli-win32-x64": "2.4.15" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.4.15.tgz", + "integrity": "sha512-rF3PPqLq1yoST79zaQbDjVJwsuIeci/O+9bgNmC5QpgOqz6aqYuzA4abyAGx+mgyiDXn4A049xAN8gijbuR1Qg==", "cpu": [ "arm64" ], @@ -240,9 +243,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.4.15.tgz", + "integrity": "sha512-/5KHXYMfSJs1fNXiX30xFtI8JcCFV6zaVVLxOa0M2sfqBKHkpQhRTv94yxQWxeTY2lzo2OuTlNvPC+hDQt2wcQ==", "cpu": [ "x64" ], @@ -257,9 +260,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.4.15.tgz", + "integrity": "sha512-owaAMZD/T4LrD0ELNCk0Km3qrRHuM0X6EAyVE1FSqGY0rbLoiDLrO4Us2tllm6cAeB2Ioa9C2C08NZPdr8+0Ug==", "cpu": [ "arm64" ], @@ -274,9 +277,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.4.15.tgz", + "integrity": "sha512-ZPcxznxm0pogHBLZhYntyR3sR+MrZjqJIKEr7ZqVen0Rl+P/4upVmfYXjftizi9RoqZntg33fv/1fbdhbYXpEQ==", "cpu": [ "arm64" ], @@ -291,9 +294,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.4.15.tgz", + "integrity": "sha512-0jj7THz12GbUOLmMibktK6DZjqz2zV64KFxyBtcFTKPiiOIY0a7vns1elpO1dERvxpsZ5ik0oFfz0oGwFde1+g==", "cpu": [ "x64" ], @@ -308,9 +311,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.4.15.tgz", + "integrity": "sha512-CNq/9W38SYSH023lfcQ4KKU8K0YX8T//FZUhcgtMMRABDojx5XsMV7jlweAvGSl389wJQB29Qo6Zb/a+jdvt+w==", "cpu": [ "x64" ], @@ -325,9 +328,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.4.15.tgz", + "integrity": "sha512-ouhkYdlhp/1GghEJPdWwD/Vi3gQ1nFxuSpMolWsbq3Lsq3QUR4jl6UdhhscdCugKU5vOEuMiJhvKj66O0OCq+w==", "cpu": [ "arm64" ], @@ -342,9 +345,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.4.15", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.4.15.tgz", + "integrity": "sha512-zBrGq5mx5wwpnow4+2BxUvleDM+GNd4sLbPaMapsSLQLD0NGRCquqPBTgN+7XkUteHvj7M+BstuI8tmnV7+HgQ==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 0c689ad6f..d1851d8cb 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "tsscmp": "^1.0.6" }, "devDependencies": { - "@biomejs/biome": "^1.9.0", + "@biomejs/biome": "^2.4.15", "@changesets/cli": "^2.29.8", "@tsconfig/node20": "^20.1.5", "@types/chai": "^4.1.7", diff --git a/src/App.ts b/src/App.ts index 4273ddf9e..ba249d5f2 100644 --- a/src/App.ts +++ b/src/App.ts @@ -1,13 +1,13 @@ import util from 'node:util'; -import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, LogLevel } from '@slack/logger'; import { + addAppMetadata, type FetchFunction, WebAPIHTTPError, WebAPIPlatformError, WebAPIRateLimitedError, WebClient, type WebClientOptions, - addAppMetadata, } from '@slack/web-api'; import type { Assistant } from './Assistant'; import { @@ -23,24 +23,25 @@ import { createSay, createSayStream, createSetStatus, + type SayStreamFn, + type SetStatusFn, } from './context'; -import type { SayStreamFn, SetStatusFn } from './context'; -import { type ConversationStore, MemoryStore, conversationContext } from './conversation-store'; +import { type ConversationStore, conversationContext, MemoryStore } from './conversation-store'; import { AppInitializationError, AuthorizationError, + asCodedError, type CodedError, InvalidCustomPropertyError, MultipleListenerError, - asCodedError, } from './errors'; import { - IncomingEventType, assertNever, extractEventChannelId, extractEventThreadTs, extractEventTs, getTypeAndConversation, + IncomingEventType, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize, } from './helpers'; @@ -99,7 +100,8 @@ import type { ViewOutput, } from './types'; import { contextBuiltinKeys } from './types'; -import { type StringIndexed, isRejected } from './types/utilities'; +import { isRejected, type StringIndexed } from './types/utilities'; + const packageJson = require('../package.json'); export type { ActionConstraints, OptionsConstraints, ShortcutConstraints, ViewConstraints } from './types'; @@ -155,7 +157,7 @@ export interface AppOptions { attachFunctionToken?: boolean; } -export { LogLevel, Logger } from '@slack/logger'; +export { Logger, LogLevel } from '@slack/logger'; /** Authorization function - seeds the middleware processing and listeners with an authorization context */ export type Authorize = ( @@ -1051,7 +1053,7 @@ export default class App // TODO: this logic should be isolated and tested according to the expected behavior if (token !== undefined) { - let pool: WebClientPool | undefined = undefined; + let pool: WebClientPool | undefined; const clientOptionsCopy = { ...this.clientOptions }; if (authorizeResult.teamId !== undefined) { pool = this.clients[authorizeResult.teamId]; @@ -1196,7 +1198,6 @@ export default class App const rejectedListenerResults = settledListenerResults.filter(isRejected); if (rejectedListenerResults.length === 1) { throw rejectedListenerResults[0].reason; - // biome-ignore lint/style/noUselessElse: I think this is a biome issue actually... } else if (rejectedListenerResults.length > 1) { throw new MultipleListenerError(rejectedListenerResults.map((rlr) => rlr.reason)); } @@ -1323,15 +1324,15 @@ export default class App throw new AppInitializationError( `${tokenUsage} \n\nSince you have not provided a token or authorize, you might be missing one or more required oauth installer options. See https://docs.slack.dev/tools/bolt-js/concepts/authenticating-oauth/ for these required fields.\n`, ); - // biome-ignore lint/style/noUselessElse: I think this is a biome issue actually... - } else if (authorize !== undefined && usingOauth) { + } + if (authorize !== undefined && usingOauth) { throw new AppInitializationError(`You cannot provide both authorize and oauth installer options. ${tokenUsage}`); - // biome-ignore lint/style/noUselessElse: I think this is a biome issue actually... - } else if (authorize === undefined && usingOauth) { + } + if (authorize === undefined && usingOauth) { // biome-ignore lint/style/noNonNullAssertion: we know installer is truthy here return httpReceiver.installer!.authorize; - // biome-ignore lint/style/noUselessElse: I think this is a biome issue actually... - } else if (authorize !== undefined && !usingOauth) { + } + if (authorize !== undefined && !usingOauth) { return authorize as Authorize; } return undefined; @@ -1623,9 +1624,9 @@ function escapeHtml(input: string | undefined | null): string { } function extractFunctionContext(body: StringIndexed) { - let functionExecutionId: string | undefined = undefined; - let functionBotAccessToken: string | undefined = undefined; - let functionInputs: FunctionInputs | undefined = undefined; + let functionExecutionId: string | undefined; + let functionBotAccessToken: string | undefined; + let functionInputs: FunctionInputs | undefined; // function_executed event if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) { diff --git a/src/Assistant.ts b/src/Assistant.ts index 4a612ecbc..f073b8897 100644 --- a/src/Assistant.ts +++ b/src/Assistant.ts @@ -9,9 +9,7 @@ import { type AssistantThreadContextStore, DefaultThreadContextStore, } from './AssistantThreadContextStore'; -import { createSayStream, createSetStatus } from './context'; -import type { SayStreamFn } from './context'; -import type { SetStatusFn } from './context'; +import { createSayStream, createSetStatus, type SayStreamFn, type SetStatusFn } from './context'; import { AssistantInitializationError, AssistantMissingPropertyError } from './errors'; import { extractEventChannelId, extractEventThreadTs, isRecord } from './helpers'; import processMiddleware from './middleware/process'; diff --git a/src/context/index.ts b/src/context/index.ts index fe0f52883..a73aee94d 100644 --- a/src/context/index.ts +++ b/src/context/index.ts @@ -1,8 +1,8 @@ +export { createFunctionComplete } from './create-function-complete'; +export { createFunctionFail } from './create-function-fail'; +export { createRespond } from './create-respond'; export { createSay } from './create-say'; +export type { SayStreamArguments, SayStreamFn } from './create-say-stream'; export { createSayStream } from './create-say-stream'; -export type { SayStreamFn, SayStreamArguments } from './create-say-stream'; +export type { SetStatusArguments, SetStatusFn } from './create-set-status'; export { createSetStatus } from './create-set-status'; -export type { SetStatusFn, SetStatusArguments } from './create-set-status'; -export { createRespond } from './create-respond'; -export { createFunctionComplete } from './create-function-complete'; -export { createFunctionFail } from './create-function-fail'; diff --git a/src/index.ts b/src/index.ts index 5dbf6fba3..31a7c15cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,25 +1,24 @@ // Import App for better ES module compatibility import AppClass from './App'; +// Re-export Logger type for TypeScript consumers +export type { Logger } from './App'; export { + ActionConstraints, AppOptions, Authorize, - AuthorizeSourceData, AuthorizeResult, - ActionConstraints, + AuthorizeSourceData, LogLevel, } from './App'; -// Re-export Logger type for TypeScript consumers -export type { Logger } from './App'; - // Export App both as named and default for better ES module compatibility export { AppClass as App }; export default AppClass; export { - verifySlackRequest, isValidSlackRequest, + verifySlackRequest, } from './receivers/verify-request'; // Import receivers first, then re-export for better ESM compatibility @@ -28,61 +27,52 @@ import ExpressReceiver from './receivers/ExpressReceiver'; import HTTPReceiver from './receivers/HTTPReceiver'; import SocketModeReceiver from './receivers/SocketModeReceiver'; -export { ExpressReceiver, SocketModeReceiver, HTTPReceiver, AwsLambdaReceiver }; -export type { ExpressReceiverOptions } from './receivers/ExpressReceiver'; -export type { SocketModeReceiverOptions } from './receivers/SocketModeReceiver'; -export type { HTTPReceiverOptions } from './receivers/HTTPReceiver'; +// Re-export OAuth types for TypeScript consumers +export type { + Installation, + InstallationQuery, + InstallationStore, + InstallProviderOptions, + InstallURLOptions, + StateStore, +} from '@slack/oauth'; +// Re-export OAuth runtime classes +export { FileInstallationStore, MemoryInstallationStore } from '@slack/oauth'; +export * as types from '@slack/types'; +export * as webApi from '@slack/web-api'; +export { + Assistant, + AssistantConfig, + AssistantThreadContextChangedMiddleware, + AssistantThreadStartedMiddleware, + AssistantUserMessageMiddleware, +} from './Assistant'; +export type { SayStreamArguments, SayStreamFn } from './context/create-say-stream'; +export type { SetStatusArguments, SetStatusFn } from './context/create-set-status'; +export { ConversationStore, MemoryStore } from './conversation-store'; +export * from './errors'; +export * from './middleware/builtin'; export type { AwsLambdaReceiverOptions } from './receivers/AwsLambdaReceiver'; - export { BufferedIncomingMessage } from './receivers/BufferedIncomingMessage'; export { - RequestVerificationOptions, + buildReceiverRoutes, + CustomRoute, + ReceiverRoutes, +} from './receivers/custom-routes'; +export type { ExpressReceiverOptions } from './receivers/ExpressReceiver'; +export * as HTTPModuleFunctions from './receivers/HTTPModuleFunctions'; +export { ReceiverDispatchErrorHandlerArgs, ReceiverProcessEventErrorHandlerArgs, ReceiverUnhandledRequestHandlerArgs, + RequestVerificationOptions, } from './receivers/HTTPModuleFunctions'; -export * as HTTPModuleFunctions from './receivers/HTTPModuleFunctions'; +export type { HTTPReceiverOptions } from './receivers/HTTPReceiver'; export { HTTPResponseAck } from './receivers/HTTPResponseAck'; - export { defaultProcessEventErrorHandler, SocketModeReceiverProcessEventErrorHandlerArgs, } from './receivers/SocketModeFunctions'; - -export * from './errors'; -export * from './middleware/builtin'; +export type { SocketModeReceiverOptions } from './receivers/SocketModeReceiver'; export * from './types'; -export type { SayStreamFn, SayStreamArguments } from './context/create-say-stream'; -export type { SetStatusFn, SetStatusArguments } from './context/create-set-status'; - -export { ConversationStore, MemoryStore } from './conversation-store'; - -export { - CustomRoute, - ReceiverRoutes, - buildReceiverRoutes, -} from './receivers/custom-routes'; - -export { - Assistant, - AssistantConfig, - AssistantThreadContextChangedMiddleware, - AssistantThreadStartedMiddleware, - AssistantUserMessageMiddleware, -} from './Assistant'; - -// Re-export OAuth runtime classes -export { MemoryInstallationStore, FileInstallationStore } from '@slack/oauth'; - -// Re-export OAuth types for TypeScript consumers -export type { - Installation, - InstallURLOptions, - InstallationQuery, - InstallationStore, - StateStore, - InstallProviderOptions, -} from '@slack/oauth'; - -export * as types from '@slack/types'; -export * as webApi from '@slack/web-api'; +export { AwsLambdaReceiver, ExpressReceiver, HTTPReceiver, SocketModeReceiver }; diff --git a/src/receivers/AwsLambdaReceiver.ts b/src/receivers/AwsLambdaReceiver.ts index 526a89575..41cd659e5 100644 --- a/src/receivers/AwsLambdaReceiver.ts +++ b/src/receivers/AwsLambdaReceiver.ts @@ -1,6 +1,6 @@ import crypto from 'node:crypto'; import querystring from 'node:querystring'; -import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, LogLevel } from '@slack/logger'; import tsscmp from 'tsscmp'; import type App from '../App'; import { ReceiverMultipleAckError } from '../errors'; diff --git a/src/receivers/ExpressReceiver.ts b/src/receivers/ExpressReceiver.ts index 1d9af88d3..e0a651ccf 100644 --- a/src/receivers/ExpressReceiver.ts +++ b/src/receivers/ExpressReceiver.ts @@ -1,14 +1,13 @@ import crypto from 'node:crypto'; -import { type Server, type ServerOptions, createServer } from 'node:http'; -import type { IncomingMessage, ServerResponse } from 'node:http'; +import { createServer, type IncomingMessage, type Server, type ServerOptions, type ServerResponse } from 'node:http'; import { + createServer as createHttpsServer, type Server as HTTPSServer, type ServerOptions as HTTPSServerOptions, - createServer as createHttpsServer, } from 'node:https'; import type { ListenOptions } from 'node:net'; import querystring from 'node:querystring'; -import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, LogLevel } from '@slack/logger'; import { type CallbackOptions, type InstallPathOptions, @@ -17,12 +16,12 @@ import { type InstallURLOptions, } from '@slack/oauth'; import express, { - type Request, - type Response, type Application, + type IRouter, + type Request, type RequestHandler, + type Response, Router, - type IRouter, } from 'express'; import rawBody from 'raw-body'; import tsscmp from 'tsscmp'; diff --git a/src/receivers/HTTPReceiver.ts b/src/receivers/HTTPReceiver.ts index d85ea502c..115554707 100644 --- a/src/receivers/HTTPReceiver.ts +++ b/src/receivers/HTTPReceiver.ts @@ -1,19 +1,19 @@ import { + createServer, type IncomingMessage, type RequestListener, type Server, type ServerOptions, type ServerResponse, - createServer, } from 'node:http'; import { + createServer as createHttpsServer, type Server as HTTPSServer, type ServerOptions as HTTPSServerOptions, - createServer as createHttpsServer, } from 'node:https'; import type { ListenOptions } from 'node:net'; import { URL } from 'node:url'; -import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, LogLevel } from '@slack/logger'; import { type CallbackOptions, type InstallPathOptions, @@ -28,10 +28,10 @@ import { type CodedError, HTTPReceiverDeferredRequestError, ReceiverInconsistent import type { Receiver, ReceiverEvent } from '../types'; import type { StringIndexed } from '../types/utilities'; import type { BufferedIncomingMessage } from './BufferedIncomingMessage'; +import { buildReceiverRoutes, type CustomRoute, type ReceiverRoutes } from './custom-routes'; import * as httpFunc from './HTTPModuleFunctions'; import { HTTPResponseAck } from './HTTPResponseAck'; import type { ParamsIncomingMessage } from './ParamsIncomingMessage'; -import { type CustomRoute, type ReceiverRoutes, buildReceiverRoutes } from './custom-routes'; import { verifyRedirectOpts } from './verify-redirect-opts'; // Option keys for tls.createServer() and tls.createSecureContext(), exclusive of those for http.createServer() diff --git a/src/receivers/SocketModeReceiver.ts b/src/receivers/SocketModeReceiver.ts index ca6b52c2e..9d3c4e2ea 100644 --- a/src/receivers/SocketModeReceiver.ts +++ b/src/receivers/SocketModeReceiver.ts @@ -1,6 +1,6 @@ -import { type Server, type ServerResponse, createServer } from 'node:http'; +import { createServer, type Server, type ServerResponse } from 'node:http'; import { URL } from 'node:url'; -import { ConsoleLogger, LogLevel, type Logger } from '@slack/logger'; +import { ConsoleLogger, type Logger, LogLevel } from '@slack/logger'; import { type CallbackOptions, type InstallPathOptions, @@ -8,8 +8,8 @@ import { type InstallProviderOptions, type InstallURLOptions, } from '@slack/oauth'; -import { SocketModeClient } from '@slack/socket-mode'; import type { SocketModeOptions } from '@slack/socket-mode'; +import { SocketModeClient } from '@slack/socket-mode'; import type { AppsConnectionsOpenResponse } from '@slack/web-api'; import type { ParamsDictionary } from 'express-serve-static-core'; import { match } from 'path-to-regexp'; @@ -17,13 +17,13 @@ import type App from '../App'; import type { CodedError } from '../errors'; import type { Receiver, ReceiverEvent } from '../types'; import type { StringIndexed } from '../types/utilities'; +import { buildReceiverRoutes, type ReceiverRoutes } from './custom-routes'; import type { ParamsIncomingMessage } from './ParamsIncomingMessage'; import { - type SocketModeReceiverProcessEventErrorHandlerArgs, defaultProcessEventErrorHandler, + type SocketModeReceiverProcessEventErrorHandlerArgs, } from './SocketModeFunctions'; import { SocketModeResponseAck } from './SocketModeResponseAck'; -import { type ReceiverRoutes, buildReceiverRoutes } from './custom-routes'; import { verifyRedirectOpts } from './verify-redirect-opts'; // TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations? diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts index fd38838cf..7a635b5a0 100644 --- a/src/types/actions/index.ts +++ b/src/types/actions/index.ts @@ -4,9 +4,10 @@ import type { AckFn, RespondFn, SayArguments, SayFn } from '../utilities'; import type { BlockAction } from './block-action'; import type { DialogSubmitAction, DialogValidation } from './dialog-action'; import type { InteractiveMessage } from './interactive-message'; + export * from './block-action'; -export * from './interactive-message'; export * from './dialog-action'; +export * from './interactive-message'; /** * All known actions from Slack's Block Kit interactive components, message actions, dialogs, and legacy interactive diff --git a/src/types/events/index.ts b/src/types/events/index.ts index b9c28e539..cdb847616 100644 --- a/src/types/events/index.ts +++ b/src/types/events/index.ts @@ -103,7 +103,6 @@ interface Authorization { * When the string matches known event(s) from the `SlackEvent` union, only those types are returned (also as a union). * Otherwise, the `BasicSlackEvent` type is returned. */ -export type EventFromType = KnownEventFromType extends never - ? BaseSlackEvent - : KnownEventFromType; +export type EventFromType = + KnownEventFromType extends never ? BaseSlackEvent : KnownEventFromType; export type KnownEventFromType = Extract; diff --git a/src/types/index.ts b/src/types/index.ts index 4d3c2d92b..80a6c7621 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,9 +1,9 @@ -export * from './utilities'; -export * from './middleware'; export * from './actions'; export * from './command'; export * from './events'; +export * from './middleware'; export * from './options'; -export * from './view'; export * from './receiver'; export * from './shortcuts'; +export * from './utilities'; +export * from './view'; diff --git a/src/types/options/index.ts b/src/types/options/index.ts index 5522f9a4c..24983c9a3 100644 --- a/src/types/options/index.ts +++ b/src/types/options/index.ts @@ -35,9 +35,8 @@ export interface BasicOptionsPayload { value: string; } // TODO: Is this useful? Events have something similar -export type OptionsPayloadFromType = KnownOptionsPayloadFromType extends never - ? BasicOptionsPayload - : KnownOptionsPayloadFromType; +export type OptionsPayloadFromType = + KnownOptionsPayloadFromType extends never ? BasicOptionsPayload : KnownOptionsPayloadFromType; export type KnownOptionsPayloadFromType = Extract; /** diff --git a/test/unit/App/basic.spec.ts b/test/unit/App/basic.spec.ts index b7a8ad78c..11ea3a2a3 100644 --- a/test/unit/App/basic.spec.ts +++ b/test/unit/App/basic.spec.ts @@ -4,9 +4,9 @@ import sinon from 'sinon'; import { ErrorCode } from '../../../src/errors'; import SocketModeReceiver from '../../../src/receivers/SocketModeReceiver'; import { - FakeReceiver, createFakeConversationStore, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noop, diff --git a/test/unit/App/middlewares/arguments.spec.ts b/test/unit/App/middlewares/arguments.spec.ts index 43c6ca8e1..b6338bfe5 100644 --- a/test/unit/App/middlewares/arguments.spec.ts +++ b/test/unit/App/middlewares/arguments.spec.ts @@ -6,8 +6,6 @@ import type { SayStreamFn } from '../../../../src/context/create-say-stream'; import type { SetStatusFn } from '../../../../src/context/create-set-status'; import type { ReceiverEvent, SayFn } from '../../../../src/types'; import { - FakeReceiver, - type Override, createDummyAppMentionEventMiddlewareArgs, createDummyBlockActionEventMiddlewareArgs, createDummyCustomFunctionMiddlewareArgs, @@ -15,10 +13,12 @@ import { createDummyReceiverEvent, createDummyViewSubmissionMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noop, noopMiddleware, + type Override, withChatStream, withConversationContext, withMemoryStore, diff --git a/test/unit/App/middlewares/global.spec.ts b/test/unit/App/middlewares/global.spec.ts index 0f834a5fe..46ab98417 100644 --- a/test/unit/App/middlewares/global.spec.ts +++ b/test/unit/App/middlewares/global.spec.ts @@ -6,11 +6,11 @@ import type { ExtendedErrorHandlerArgs } from '../../../../src/App'; import { AuthorizationError, type CodedError, ErrorCode, UnknownError } from '../../../../src/errors'; import type { NextFn, ReceiverEvent } from '../../../../src/types'; import { - FakeReceiver, createDummyCustomFunctionMiddlewareArgs, createDummyReceiverEvent, createFakeLogger, delay, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, diff --git a/test/unit/App/middlewares/ignore-self.spec.ts b/test/unit/App/middlewares/ignore-self.spec.ts index 4f6d68217..da6be9d50 100644 --- a/test/unit/App/middlewares/ignore-self.spec.ts +++ b/test/unit/App/middlewares/ignore-self.spec.ts @@ -1,15 +1,15 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import { - FakeReceiver, - type Override, createDummyMemberChannelEventMiddlewareArgs, createDummyMessageEventMiddlewareArgs, createDummyReactionAddedEventMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/middlewares/listener.spec.ts b/test/unit/App/middlewares/listener.spec.ts index f6c4815ba..9d162ee84 100644 --- a/test/unit/App/middlewares/listener.spec.ts +++ b/test/unit/App/middlewares/listener.spec.ts @@ -2,7 +2,7 @@ import { assert } from 'chai'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../../src/App'; import { ErrorCode, isCodedError } from '../../../../src/errors'; -import { FakeReceiver, createDummyReceiverEvent, importApp } from '../../helpers'; +import { createDummyReceiverEvent, FakeReceiver, importApp } from '../../helpers'; describe('App listener middleware processing', () => { let fakeReceiver: FakeReceiver; diff --git a/test/unit/App/routing-action.spec.ts b/test/unit/App/routing-action.spec.ts index 5415e7468..b4b0bc4c7 100644 --- a/test/unit/App/routing-action.spec.ts +++ b/test/unit/App/routing-action.spec.ts @@ -2,14 +2,14 @@ import { assert } from 'chai'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyBlockActionEventMiddlewareArgs, createDummyFunctionScopedBlockActionEventMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, @@ -87,7 +87,7 @@ describe('App action() routing', () => { }); it('should throw if provided a constraint with unknown action constraint keys', async () => { - // @ts-ignore providing known invalid action constraint parameter + // @ts-expect-error providing known invalid action constraint parameter app.action({ id: 'boom' }, fakeHandler); sinon.assert.calledWithMatch(fakeLogger.error, 'unknown constraint keys'); }); diff --git a/test/unit/App/routing-assistant.spec.ts b/test/unit/App/routing-assistant.spec.ts index 2d6c96863..104d9438b 100644 --- a/test/unit/App/routing-assistant.spec.ts +++ b/test/unit/App/routing-assistant.spec.ts @@ -2,15 +2,15 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { Assistant } from '../../../src/Assistant'; import { - FakeReceiver, - type Override, createDummyAssistantThreadContextChangedEventMiddlewareArgs, createDummyAssistantThreadStartedEventMiddlewareArgs, createDummyAssistantUserMessageEventMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-command.spec.ts b/test/unit/App/routing-command.spec.ts index 69467ae86..999199b06 100644 --- a/test/unit/App/routing-command.spec.ts +++ b/test/unit/App/routing-command.spec.ts @@ -1,13 +1,13 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyCommandMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-event.spec.ts b/test/unit/App/routing-event.spec.ts index bae907b7f..c7fb22b60 100644 --- a/test/unit/App/routing-event.spec.ts +++ b/test/unit/App/routing-event.spec.ts @@ -2,13 +2,13 @@ import assert from 'node:assert'; import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyAppMentionEventMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-function.spec.ts b/test/unit/App/routing-function.spec.ts index 90db27f40..b894b47fd 100644 --- a/test/unit/App/routing-function.spec.ts +++ b/test/unit/App/routing-function.spec.ts @@ -2,13 +2,13 @@ import { assert } from 'chai'; import sinon from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyCustomFunctionMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-message.spec.ts b/test/unit/App/routing-message.spec.ts index b85013fcf..fbe263b43 100644 --- a/test/unit/App/routing-message.spec.ts +++ b/test/unit/App/routing-message.spec.ts @@ -1,13 +1,13 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyMessageEventMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-options.spec.ts b/test/unit/App/routing-options.spec.ts index 598727611..72f090130 100644 --- a/test/unit/App/routing-options.spec.ts +++ b/test/unit/App/routing-options.spec.ts @@ -1,13 +1,13 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyBlockSuggestionsMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, diff --git a/test/unit/App/routing-shortcut.spec.ts b/test/unit/App/routing-shortcut.spec.ts index ce546dc10..9374d2efa 100644 --- a/test/unit/App/routing-shortcut.spec.ts +++ b/test/unit/App/routing-shortcut.spec.ts @@ -1,13 +1,13 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyMessageShortcutMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, @@ -94,7 +94,7 @@ describe('App shortcut() routing', () => { }); it('should throw if provided a constraint with unknown shortcut constraint keys', async () => { - // @ts-ignore providing known invalid shortcut constraint parameter + // @ts-expect-error providing known invalid shortcut constraint parameter app.shortcut({ id: 'boom' }, fakeHandler); sinon.assert.calledWithMatch(fakeLogger.error, 'unknown constraint keys'); }); diff --git a/test/unit/App/routing-view.spec.ts b/test/unit/App/routing-view.spec.ts index 5f10d1262..de43b6d95 100644 --- a/test/unit/App/routing-view.spec.ts +++ b/test/unit/App/routing-view.spec.ts @@ -1,14 +1,14 @@ import sinon, { type SinonSpy } from 'sinon'; import type App from '../../../src/App'; import { - FakeReceiver, - type Override, createDummyViewClosedMiddlewareArgs, createDummyViewSubmissionMiddlewareArgs, createFakeLogger, + FakeReceiver, importApp, mergeOverrides, noopMiddleware, + type Override, withConversationContext, withMemoryStore, withNoopAppMetadata, @@ -49,7 +49,7 @@ describe('App view() routing', () => { }); it('should throw if provided a constraint with unknown view constraint keys', async () => { - // @ts-ignore providing known invalid view constraint parameter + // @ts-expect-error providing known invalid view constraint parameter app.view({ id: 'boom' }, fakeHandler); sinon.assert.calledWithMatch(fakeLogger.error, 'unknown constraint keys'); }); diff --git a/test/unit/Assistant.spec.ts b/test/unit/Assistant.spec.ts index 6818bae5c..a81ae55f8 100644 --- a/test/unit/Assistant.spec.ts +++ b/test/unit/Assistant.spec.ts @@ -14,12 +14,12 @@ import type { AssistantThreadContext, AssistantThreadContextStore } from '../../ import { AssistantInitializationError, AssistantMissingPropertyError } from '../../src/errors'; import type { Middleware } from '../../src/types'; import { - type Override, createDummyAppMentionEventMiddlewareArgs, createDummyAssistantThreadContextChangedEventMiddlewareArgs, createDummyAssistantThreadStartedEventMiddlewareArgs, createDummyAssistantUserMessageEventMiddlewareArgs, createDummyMessageEventMiddlewareArgs, + type Override, proxyquire, wrapMiddleware, } from './helpers'; diff --git a/test/unit/CustomFunction.spec.ts b/test/unit/CustomFunction.spec.ts index 5aa123579..fcff37321 100644 --- a/test/unit/CustomFunction.spec.ts +++ b/test/unit/CustomFunction.spec.ts @@ -1,8 +1,8 @@ import { assert } from 'chai'; import { CustomFunction, - type SlackCustomFunctionMiddlewareArgs, matchCallbackId, + type SlackCustomFunctionMiddlewareArgs, validate, } from '../../src/CustomFunction'; import { CustomFunctionInitializationError } from '../../src/errors'; diff --git a/test/unit/conversation-store.spec.ts b/test/unit/conversation-store.spec.ts index e677c0ed5..a50412a21 100644 --- a/test/unit/conversation-store.spec.ts +++ b/test/unit/conversation-store.spec.ts @@ -1,10 +1,10 @@ import path from 'node:path'; import type { Logger } from '@slack/logger'; import type { WebClient } from '@slack/web-api'; -import { assert, AssertionError } from 'chai'; +import { AssertionError, assert } from 'chai'; import sinon, { type SinonSpy } from 'sinon'; import type { AnyMiddlewareArgs, Context, NextFn } from '../../src/types'; -import { type Override, createFakeLogger, delay, proxyquire } from './helpers'; +import { createFakeLogger, delay, type Override, proxyquire } from './helpers'; /* Testing Harness */ diff --git a/test/unit/errors.spec.ts b/test/unit/errors.spec.ts index ec907eaa2..d65f69ee7 100644 --- a/test/unit/errors.spec.ts +++ b/test/unit/errors.spec.ts @@ -2,13 +2,13 @@ import { assert } from 'chai'; import { AppInitializationError, AuthorizationError, + asCodedError, type CodedError, ContextMissingPropertyError, ErrorCode, ReceiverAuthenticityError, ReceiverMultipleAckError, UnknownError, - asCodedError, } from '../../src/errors'; describe('Errors', () => { diff --git a/test/unit/helpers.spec.ts b/test/unit/helpers.spec.ts index 688d23069..c746cf99b 100644 --- a/test/unit/helpers.spec.ts +++ b/test/unit/helpers.spec.ts @@ -1,11 +1,11 @@ import { assert } from 'chai'; import { - IncomingEventType, extractEventChannelId, extractEventThreadTs, extractEventTs, getTypeAndConversation, hasStringProperty, + IncomingEventType, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize, isRecord, diff --git a/test/unit/helpers/events.ts b/test/unit/helpers/events.ts index 1576bf43f..19bf24670 100644 --- a/test/unit/helpers/events.ts +++ b/test/unit/helpers/events.ts @@ -11,7 +11,6 @@ import type { } from '@slack/types'; import { WebClient } from '@slack/web-api'; import sinon, { type SinonSpy } from 'sinon'; -import { createFakeLogger } from '.'; import type { AssistantThreadContextChangedMiddlewareArgs, AssistantThreadStartedMiddlewareArgs, @@ -48,6 +47,7 @@ import type { ViewOutput, ViewSubmitAction, } from '../../../src/types'; +import { createFakeLogger } from '.'; const ts = '1234.56'; const user = 'U1234'; diff --git a/test/unit/helpers/index.ts b/test/unit/helpers/index.ts index 91e340b95..0d7c46b02 100644 --- a/test/unit/helpers/index.ts +++ b/test/unit/helpers/index.ts @@ -6,11 +6,11 @@ import type { NextFn } from '../../../src/types'; // Ensure that the module gets loaded fresh every time proxyquire.noPreserveCache(); -export { proxyquire }; export * from './app'; export * from './events'; export * from './receivers'; +export { proxyquire }; export function createFakeLogger() { return sinon.createStubInstance(ConsoleLogger); diff --git a/test/unit/middleware/builtin.spec.ts b/test/unit/middleware/builtin.spec.ts index 9d9422531..9badf9368 100644 --- a/test/unit/middleware/builtin.spec.ts +++ b/test/unit/middleware/builtin.spec.ts @@ -6,13 +6,13 @@ import { ErrorCode } from '../../../src/errors'; import { isSlackEventMiddlewareArgsOptions } from '../../../src/middleware/builtin'; import type { Context, SlackEventMiddlewareArgs, SlackEventMiddlewareArgsOptions } from '../../../src/types'; import { - type Override, createDummyAppHomeOpenedEventMiddlewareArgs, createDummyAppMentionEventMiddlewareArgs, createDummyCommandMiddlewareArgs, createDummyMemberChannelEventMiddlewareArgs, createDummyMessageEventMiddlewareArgs, createDummyReactionAddedEventMiddlewareArgs, + type Override, proxyquire, wrapMiddleware, } from '../helpers'; @@ -154,7 +154,7 @@ describe('Built-in global middleware', () => { const ctx = { ...dummyContext }; const args = wrapMiddleware(createDummyMessageEventMiddlewareArgs(), ctx); - let error: Error | undefined = undefined; + let error: Error | undefined; try { await builtins.directMention(args); } catch (err) { diff --git a/test/unit/receivers/AwsLambdaReceiver.spec.ts b/test/unit/receivers/AwsLambdaReceiver.spec.ts index ba14c3fc6..43c5cf3a1 100644 --- a/test/unit/receivers/AwsLambdaReceiver.spec.ts +++ b/test/unit/receivers/AwsLambdaReceiver.spec.ts @@ -3,8 +3,8 @@ import { assert } from 'chai'; import sinon from 'sinon'; import AwsLambdaReceiver from '../../../src/receivers/AwsLambdaReceiver'; import { - createDummyAWSPayload, createDummyAppMentionEventMiddlewareArgs, + createDummyAWSPayload, createFakeLogger, importApp, mergeOverrides, diff --git a/test/unit/receivers/ExpressReceiver.spec.ts b/test/unit/receivers/ExpressReceiver.spec.ts index f5ec2e41f..f131aaa52 100644 --- a/test/unit/receivers/ExpressReceiver.spec.ts +++ b/test/unit/receivers/ExpressReceiver.spec.ts @@ -14,18 +14,18 @@ import { ReceiverInconsistentStateError, } from '../../../src/errors'; import ExpressReceiver, { + buildBodyParserMiddleware, respondToSslCheck, respondToUrlVerification, verifySignatureAndParseRawBody, - buildBodyParserMiddleware, } from '../../../src/receivers/ExpressReceiver'; import * as httpFunc from '../../../src/receivers/HTTPModuleFunctions'; import type { ReceiverEvent } from '../../../src/types'; import { - FakeServer, - type Override, createFakeLogger, + FakeServer, mergeOverrides, + type Override, proxyquire, withHttpCreateServer, withHttpsCreateServer, diff --git a/test/unit/receivers/HTTPReceiver.spec.ts b/test/unit/receivers/HTTPReceiver.spec.ts index 85b933a3f..8b3e15a09 100644 --- a/test/unit/receivers/HTTPReceiver.spec.ts +++ b/test/unit/receivers/HTTPReceiver.spec.ts @@ -12,11 +12,11 @@ import { } from '../../../src/errors'; import type { CustomRoute } from '../../../src/receivers/custom-routes'; import { - FakeServer, - type Override, createFakeLogger, + FakeServer, mergeOverrides, type noopVoid, + type Override, proxyquire, withHttpCreateServer, withHttpsCreateServer, diff --git a/test/unit/receivers/SocketModeReceiver.spec.ts b/test/unit/receivers/SocketModeReceiver.spec.ts index 0c8568c65..f76b8f2de 100644 --- a/test/unit/receivers/SocketModeReceiver.spec.ts +++ b/test/unit/receivers/SocketModeReceiver.spec.ts @@ -12,12 +12,12 @@ import { AppInitializationError, AuthorizationError, CustomRouteInitializationEr import { defaultProcessEventErrorHandler } from '../../../src/receivers/SocketModeFunctions'; import type { ReceiverEvent } from '../../../src/types'; import { - FakeServer, - type Override, createFakeLogger, delay, + FakeServer, mergeOverrides, type noopVoid, + type Override, proxyquire, withHttpCreateServer, withHttpsCreateServer, From 397b7d6b61eeee41024b137f1fe743c344540d8e Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 19 May 2026 13:14:48 -0400 Subject: [PATCH 07/11] chore: improve test coverage --- test/unit/App/default-error-handler.spec.ts | 102 ++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 test/unit/App/default-error-handler.spec.ts diff --git a/test/unit/App/default-error-handler.spec.ts b/test/unit/App/default-error-handler.spec.ts new file mode 100644 index 000000000..ef04f2d61 --- /dev/null +++ b/test/unit/App/default-error-handler.spec.ts @@ -0,0 +1,102 @@ +import assert from 'node:assert'; +import { WebAPIHTTPError, WebAPIPlatformError, WebAPIRateLimitedError } from '@slack/web-api'; +import sinon from 'sinon'; +import type App from '../../../src/App'; +import type { ReceiverEvent } from '../../../src/types'; +import { + createDummyReceiverEvent, + createFakeLogger, + FakeReceiver, + importApp, + mergeOverrides, + noopMiddleware, + withConversationContext, + withMemoryStore, + withNoopAppMetadata, + withNoopWebClient, +} from '../helpers'; + +const overrides = mergeOverrides( + withNoopAppMetadata(), + withNoopWebClient(), + withMemoryStore(sinon.fake()), + withConversationContext(sinon.fake.returns(noopMiddleware)), +); + +describe('App default error handler', () => { + let fakeReceiver: FakeReceiver; + let dummyReceiverEvent: ReceiverEvent; + let app: App; + let fakeLogger: ReturnType; + + beforeEach(async () => { + fakeReceiver = new FakeReceiver(); + fakeLogger = createFakeLogger(); + dummyReceiverEvent = createDummyReceiverEvent(); + + const MockApp = importApp(overrides); + app = new MockApp({ + logger: fakeLogger, + receiver: fakeReceiver, + authorize: sinon.fake.resolves({ botToken: '', botId: '' }), + }); + }); + + it('should log a formatted message for WebAPIPlatformError', async () => { + app.use(() => { + throw new WebAPIPlatformError({ ok: false, error: 'channel_not_found' }); + }); + + try { + await fakeReceiver.sendEvent(dummyReceiverEvent); + assert.fail('should have thrown'); + } catch (_) { + assert.ok(fakeLogger.error.calledOnce); + assert.strictEqual(fakeLogger.error.firstCall.args[0], 'Slack API error: channel_not_found'); + } + }); + + it('should log a formatted message for WebAPIRateLimitedError', async () => { + app.use(() => { + throw new WebAPIRateLimitedError(30); + }); + + try { + await fakeReceiver.sendEvent(dummyReceiverEvent); + assert.fail('should have thrown'); + } catch (_) { + assert.ok(fakeLogger.error.calledOnce); + assert.strictEqual(fakeLogger.error.firstCall.args[0], 'Rate limited, retry after 30s'); + } + }); + + it('should log a formatted message for WebAPIHTTPError', async () => { + app.use(() => { + throw new WebAPIHTTPError(500, 'Internal Server Error', {}, ''); + }); + + try { + await fakeReceiver.sendEvent(dummyReceiverEvent); + assert.fail('should have thrown'); + } catch (_) { + assert.ok(fakeLogger.error.calledOnce); + assert.strictEqual(fakeLogger.error.firstCall.args[0], 'HTTP error 500: Internal Server Error'); + } + }); + + it('should log the raw error for unknown error types', async () => { + app.use(() => { + throw new Error('something unexpected'); + }); + + try { + await fakeReceiver.sendEvent(dummyReceiverEvent); + assert.fail('should have thrown'); + } catch (_) { + assert.ok(fakeLogger.error.calledOnce); + const loggedArg = fakeLogger.error.firstCall.args[0]; + assert.ok('code' in loggedArg); + assert.strictEqual(loggedArg.message, 'something unexpected'); + } + }); +}); From 017792a3810d2cce15b8f9e5026119e99518d9cf Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 16 Jun 2026 12:47:06 -0400 Subject: [PATCH 08/11] feat!: replace axios with native fetch for response_url calls --- .changeset/use-native-fetch.md | 4 +++- AGENTS.md | 2 +- docs/english/reference.md | 3 +-- src/context/create-respond.ts | 3 ++- src/errors.ts | 13 ++++++++++++ test/unit/context/create-respond.spec.ts | 25 ++++++++++++++++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) diff --git a/.changeset/use-native-fetch.md b/.changeset/use-native-fetch.md index 19f2dc9e0..bf2378116 100644 --- a/.changeset/use-native-fetch.md +++ b/.changeset/use-native-fetch.md @@ -2,4 +2,6 @@ "@slack/bolt": major --- -Replace axios with native fetch for response_url calls. Remove `agent` and `clientTls` options from `AppOptions` — use `clientOptions.fetch` to provide a custom fetch implementation for proxy/TLS needs. Add `dispatcher` option to `SocketModeReceiver` for proxy/TLS configuration in socket mode. +Replace axios with native fetch for response_url calls. Remove `agent` and `clientTls` options from `AppOptions` — use `clientOptions.fetch` to provide a custom fetch implementation for proxy/TLS needs. Add a `dispatcher` option to `SocketModeReceiver` for proxy/TLS configuration in socket mode. + +`respond()` now throws a `RespondError` when the `response_url` request returns a non-2xx status (restoring the throw-on-failure behavior that axios provided) and resolves to a `Response` on success rather than an axios response object. diff --git a/AGENTS.md b/AGENTS.md index 32f728f65..084ccfb5b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,7 +125,7 @@ Listeners receive a single object with these properties (availability depends on 4. **Don't duplicate `package.json` values** -- reference it for versions, engines, and dependency lists. 5. **Don't add `WorkflowStep` code** -- it is deprecated. Use `CustomFunction` and `app.function()` instead. 6. **Build before running unit tests directly** -- `npm test` handles this automatically, but `npm run test:unit` requires a build to exist first. -7. **Keep the Receiver abstraction clean** -- receivers should only handle transport concerns (ingesting events, sending ack responses). Business logic belongs in middleware and listeners. +7. **Keep the Receiver abstraction clean** -- receivers should only handle transport concerns (ingesting events, sending ack responses). Business logic belongs in middleware and listeners. Do **not** add receiver-specific options to `AppOptions`: configuration that only a particular receiver understands (e.g. proxy/TLS via an undici `dispatcher` for Socket Mode) must be set on the receiver instance, which is then passed to `App` via the `receiver` option. The long-term goal is for `App` to be fully agnostic of which receiver is in use, so avoid growing the App's surface with transport concerns. 8. **Prefer middleware for cross-cutting concerns** -- authorization, logging, validation, and feature-level request handling (like `Assistant`) all use the middleware pattern. 9. **TypeScript types are part of the API** -- changes to exported types are breaking changes. Add type tests for new public types. 10. **Every listener type needs four things:** type definitions, built-in middleware matchers, an App method, and tests. diff --git a/docs/english/reference.md b/docs/english/reference.md index b9765081f..390dea6f9 100644 --- a/docs/english/reference.md +++ b/docs/english/reference.md @@ -129,8 +129,6 @@ App options are passed into the `App` constructor. When the `receiver` argument | Option | Description | | :--- | :--- | | `receiver` | An instance of `Receiver` that parses and handles incoming events. Must conform to the [`Receiver` interface](/tools/bolt-js/concepts/receiver), which includes `init(app)`, `start()`, and `stop()`. More information about receivers is [in the documentation](/tools/bolt-js/concepts/receiver). | -| `agent` | Optional HTTP `Agent` used to set up proxy support. Read more about custom agents in the [Node Slack SDK documentation](/tools/node-slack-sdk/web-api#proxy-requests-with-a-custom-agent). | -| `clientTls` | Optional `string` to set a custom TLS configuration for HTTP client requests. Must be one of: `"pfx"`, `"key"`, `"passphrase"`, `"cert"`, or `"ca"`. | | `convoStore` | A store to set and retrieve state-related conversation information. `set()` sets conversation state and `get()` fetches it. By default, apps have access to an in-memory store. More information and an example can be found [in the documentation](/tools/bolt-js/legacy/conversation-store). | | `token` | A `string` from your app's configuration (under "Settings" > "Install App") required for calling the Web API. May not be passed when using `authorize`, `orgAuthorize`, or OAuth. | | `botId` | Can only be used when `authorize` is not defined. The optional `botId` is the ID for your bot token (ex: `B12345`) which can be used to ignore messages sent by your app. If a `xoxb-` token is passed to your app, this value will automatically be retrieved by your app calling the [`auth.test` method](/reference/methods/auth.test). | @@ -141,6 +139,7 @@ App options are passed into the `App` constructor. When the `receiver` argument | `extendedErrorHandler` | Option that accepts a `boolean` value. When set to `true`, the global error handler is passed an object with additional request context. Available from version 3.8.0, defaults to `false`. More information on advanced error handling can be found [in the documentation](/tools/bolt-js/concepts/error-handling). | | `ignoreSelf` | `boolean` to enable a middleware function that ignores any messages coming from your app. Requires a `botId`. Defaults to `true`. | | `clientOptions.slackApiUrl` | Allows setting a custom endpoint for the Slack API. Used most often for testing. | +| `clientOptions.fetch` | A custom `fetch` implementation (conforming to the WHATWG Fetch standard) used for all HTTP requests to Slack Web API calls, OAuth, and `respond()`. Use this to configure proxy or custom TLS behavior (for example, by wrapping a request with an undici `Dispatcher`). A global proxy can alternatively be configured with `http.setGlobalProxyFromEnv()`. | | `socketMode` | Option that accepts a `boolean` value. When set to `true` the app is started in [Socket Mode](/tools/bolt-js/concepts/socket-mode), i.e. it allows your app to connect and receive data from Slack via a WebSocket connection. Defaults to `false`. | `developerMode` | `boolean` to activate the developer mode. When set to `true` the `logLevel` is automatically set to `DEBUG` and `socketMode` is set to `true`. However, explicitly setting these two properties takes precedence over implicitly setting them via `developerMode`. Furthermore, a custom OAuth failure handler is provided to help debugging. Finally, the body of all incoming requests are logged and thus sensitive information like tokens might be contained in the logs. Defaults to `false`. | | `deferInitialization` | `boolean` to defer initialization of the app and places responsibility for manually calling the `async` `App#init()` method on the developer. `init()` must be called before `App#start()`. Defaults to `false`. | diff --git a/src/context/create-respond.ts b/src/context/create-respond.ts index ca2a81b87..febbff45d 100644 --- a/src/context/create-respond.ts +++ b/src/context/create-respond.ts @@ -1,10 +1,11 @@ import type { FetchFunction } from '@slack/web-api'; +import { RespondError } from '../errors'; import type { RespondArguments, RespondFn } from '../types'; export function createRespond(fetchFn: FetchFunction, responseUrl: string): RespondFn { return async (message: string | RespondArguments) => { const normalizedArgs: RespondArguments = typeof message === 'string' ? { text: message } : message; - return fetchFn(responseUrl, { + const response = await fetchFn(responseUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(normalizedArgs), diff --git a/src/errors.ts b/src/errors.ts index 40b1be79e..098a939d6 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -36,6 +36,8 @@ export enum ErrorCode { HTTPReceiverDeferredRequestError = 'slack_bolt_http_receiver_deferred_request_error', + RespondError = 'slack_bolt_respond_error', + /** * This value is used to assign to errors that occur inside the framework but do not have a code, to keep interfaces * in terms of CodedError. @@ -140,6 +142,17 @@ export class HTTPReceiverDeferredRequestError extends Error implements CodedErro } } +export class RespondError extends Error implements CodedError { + public code = ErrorCode.RespondError; + + public statusCode: number; + + public constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + export class MultipleListenerError extends Error implements CodedError { public code = ErrorCode.MultipleListenerError; diff --git a/test/unit/context/create-respond.spec.ts b/test/unit/context/create-respond.spec.ts index fe02d1259..adfee2da2 100644 --- a/test/unit/context/create-respond.spec.ts +++ b/test/unit/context/create-respond.spec.ts @@ -2,6 +2,7 @@ import type { FetchFunction } from '@slack/web-api'; import { assert } from 'chai'; import sinon from 'sinon'; import { createRespond } from '../../../src/context'; +import { ErrorCode, type RespondError } from '../../../src/errors'; describe('createRespond', () => { it('should post to the response URL with text when given a string', async () => { @@ -39,4 +40,28 @@ describe('createRespond', () => { assert(fakeFetch.calledOnce); assert.equal(fakeFetch.firstCall.args[0], url); }); + + it('should return the response when the request succeeds', async () => { + const response = new Response(null, { status: 200 }); + const fakeFetch = sinon.fake.resolves(response); + const respond = createRespond(fakeFetch as unknown as FetchFunction, 'https://hooks.slack.com/response/123'); + + const result = await respond('hello'); + + assert.equal(result, response); + }); + + it('should throw a RespondError when the response is not ok', async () => { + const fakeFetch = sinon.fake.resolves(new Response(null, { status: 404, statusText: 'Not Found' })); + const respond = createRespond(fakeFetch as unknown as FetchFunction, 'https://hooks.slack.com/response/123'); + + try { + await respond('hello'); + assert.fail('Expected respond to throw'); + } catch (error) { + const respondError = error as RespondError; + assert.equal(respondError.code, ErrorCode.RespondError); + assert.equal(respondError.statusCode, 404); + } + }); }); From 84150fddc94981b25b34c79e352a671c87538b33 Mon Sep 17 00:00:00 2001 From: Luke Russell <31357343+lukegalbraithrussell@users.noreply.github.com> Date: Tue, 16 Jun 2026 09:48:22 -0700 Subject: [PATCH 09/11] Docs: V5 migration (#2949) Co-authored-by: William Bergamin --- docs/english/migration/migration-v5.md | 280 +++++++++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/english/migration/migration-v5.md diff --git a/docs/english/migration/migration-v5.md b/docs/english/migration/migration-v5.md new file mode 100644 index 000000000..007ab6830 --- /dev/null +++ b/docs/english/migration/migration-v5.md @@ -0,0 +1,280 @@ +--- +sidebar_label: Migrating to v5 +--- + +# Migrating @slack/bolt from v4 to v5 + +_Minimum Node.js version: 20_ + +Bolt for JS v5 follows the Node Slack SDK's shift from axios to the native [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). It also removes the deprecated Workflow Steps feature (retired by Slack in September 2024) and raises the minimum Node.js version to 20. + +All internal `@slack/*` Node Slack SDK dependencies have been bumped to their next major versions. See the section below on [Upgrading the Node Slack SDK dependencies]({#node-slack-sdk-dependencies}) for information and migration instructions. + +If your app doesn't use proxy/TLS configuration or inspect `respond()` utility function return values, this upgrade is likely a version bump and done. + +--- + +## Breaking Changes {#breaking-changes} + +### We've raised the minimum Node.js version to 20 {#minimum-node-version} + +We've dropped support for Node.js 18. Node.js 20 or later is required. + +--- + +### We've removed the `agent` and `clientTls` options from the `AppOptions` interface {#removed-agent-clienttls} + +You should configure transport via the `clientOptions.fetch` option or use the Node.js built-in proxy support. + +**Before (v4):** + +```typescript +import { App } from '@slack/bolt'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import fs from 'node:fs'; + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + agent: new HttpsProxyAgent('http://corporate.proxy:8080'), + clientTls: { + cert: fs.readFileSync('/path/to/client-cert.pem'), + key: fs.readFileSync('/path/to/client-key.pem'), + }, +}); +``` + +#### Preferred: Built-in proxy support {#built-in-proxy-support} + +Node.js can read proxy environment variables natively via [`http.setGlobalProxyFromEnv()`](https://nodejs.org/docs/latest/api/http.html#httpsetglobalproxyfromenvproxyenv). Call it once at startup and `globalThis.fetch` routes through your proxy automatically, no extra packages needed. + +##### Option A: programmatically call once at startup {#programmatically-call-startup} + +```typescript +import http from 'node:http'; +import { App } from '@slack/bolt'; + +http.setGlobalProxyFromEnv(); + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, +}); +``` + +##### Option B: use an environment variable {#use-an-environment-variable} + +```bash +NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://corporate.proxy:8080 node app.js +``` + +```typescript +import { App } from '@slack/bolt'; + +// No proxy configuration needed — globalThis.fetch respects the environment +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, +}); +``` + +#### Alternative: use an undici `Dispatcher` instance for proxy and TLS {#undici-dispatcher-proxy-tls} + +If you need per-client configuration, use the `clientOptions.fetch` option with an [undici](https://undici.nodejs.org/) `Dispatcher` instance: + +```typescript +import { App } from '@slack/bolt'; +import { fetch, ProxyAgent, Agent } from 'undici'; +import fs from 'node:fs'; + +// Proxy only +const proxyDispatcher = new ProxyAgent('http://corporate.proxy:8080'); + +// TLS only +const tlsDispatcher = new Agent({ + connect: { + cert: fs.readFileSync('/path/to/client-cert.pem'), + key: fs.readFileSync('/path/to/client-key.pem'), + ca: fs.readFileSync('/path/to/ca-cert.pem'), + }, +}); + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + signingSecret: process.env.SLACK_SIGNING_SECRET, + clientOptions: { + fetch: (url, init) => fetch(url, { ...init, dispatcher: tlsDispatcher }), + }, +}); +``` + +--- + +### We've updated the `SocketModeReceiver` class to accept a `dispatcher` option instead of proxy agents {#socketmodereceiver-dispatcher} + +The `SocketModeReceiver` class now accepts a `dispatcher` option for unified proxy and TLS configuration of both the WebSocket connection and HTTP API calls. The `dispatcher` option accepts any undici-compatible `Dispatcher` instance. + +**Before (v4):** + +```typescript +import { App } from '@slack/bolt'; +import { HttpsProxyAgent } from 'https-proxy-agent'; + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + appToken: process.env.SLACK_APP_TOKEN, + socketMode: true, + agent: new HttpsProxyAgent('http://corporate.proxy:8080'), +}); +``` + +#### Preferred: Use built-in proxy support {#socketmode-built-in-proxy-support} + +Node.js can read proxy environment variables natively via [`http.setGlobalProxyFromEnv()`](https://nodejs.org/docs/latest/api/http.html#httpsetglobalproxyfromenvproxyenv). Call it once at startup and both the WebSocket connection and API calls route through your proxy automatically. + +```typescript +import http from 'node:http'; +import { App } from '@slack/bolt'; + +http.setGlobalProxyFromEnv(); + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + appToken: process.env.SLACK_APP_TOKEN, + socketMode: true, +}); +``` + +#### Alternative: Use an undici `Dispatcher` instance for proxy and TLS {#socketmode-undici-dispatcher-proxy-tls} + +If you need per-client configuration, pass a `dispatcher` option to both the `SocketModeReceiver` class (for the WebSocket connection) and the `clientOptions.fetch` option (for the app's internal `WebClient` class API calls): + +```typescript +import { App, SocketModeReceiver } from '@slack/bolt'; +import { fetch, ProxyAgent } from 'undici'; + +const dispatcher = new ProxyAgent('http://corporate.proxy:8080'); + +const receiver = new SocketModeReceiver({ + appToken: process.env.SLACK_APP_TOKEN, + dispatcher, +}); + +const app = new App({ + token: process.env.SLACK_BOT_TOKEN, + receiver, + clientOptions: { + fetch: (url, init) => fetch(url, { ...init, dispatcher }), + }, +}); +``` + +--- + +### We've removed Workflow Steps from Apps {#removed-workflow-steps} + +The Workflow Steps from Apps feature [was retired in September 2024](/changelog/2023-08-workflow-steps-from-apps-step-back/). The `WorkflowStep` class, the `app.step()` method, and all related types have been deleted from Bolt for JS. You should remove any imports of the `WorkflowStep` class or the `WorkflowStepEdit` type. + +Use the `app.function()` method with custom functions instead. + +--- + +### We've updated the `respond()` utility function to return a native `Response` object {#respond-returns-fetch-response} + +The `respond()` utility function now uses native fetch internally. If you inspect the return value, the shape has changed from an `AxiosResponse` object to a standard Fetch [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) object. + +**Before (v4):** + +```typescript +app.command('/ticket', async ({ command, ack, respond }) => { + await ack(); + const result = await respond(`Ticket created: ${command.text}`); + // result was an AxiosResponse + console.log(result.status); // 200 + console.log(result.data); // response body (pre-parsed) + console.log(result.headers); // AxiosHeaders object +}); +``` + +**After (v5):** + +```typescript +app.command('/ticket', async ({ command, ack, respond }) => { + await ack(); + const result = await respond(`Ticket created: ${command.text}`); + // result is a native Fetch Response + console.log(result.status); // 200 + console.log(await result.text()); // response body (call .text() or .json()) + console.log(result.headers); // Headers object +}); +``` + +If you're only calling `await respond(...)` without using the return value (the common case), no changes are needed. + +--- + +### We've upgraded the Node Slack SDK dependencies {#node-slack-sdk-dependencies} + +All Node Slack SDK packages have been bumped to their next major versions. + +* The `logger` package has been updated to v5. +* The `oauth` package has been updated to v4. +* The `types` package has been updated to v3. + +Three packages have more substantial breaking changes: + +* The `socket-mode` package has been updated to v3. See the guide on [migrating @slack/socket-mode from v2 to v3](/tools/node-slack-sdk/migration/socket-mode/migrating-socket-mode-package-to-v3) for handling breaking changes. +* The `web-api` package has been updated to v8. See the guide on [migrating @slack/web-api from v7 to v8](/tools/node-slack-sdk/migration/web-api/migrating-web-api-package-to-v8) for handling breaking changes. + +--- + +### We've improved error handling throughout {#error-handling} + +This version of Bolt leverages the new error classes from v8 of the `@slack/web-api` Node Slack SDK package. Errors thrown by the internal `WebClient` class are now proper `Error` subclasses. + +**Before (v4):** + +```typescript +import { App } from '@slack/bolt'; + +const app = new App({ /* ... */ }); + +app.error(async ({ error }) => { + if ('code' in error && error.code === 'slack_webapi_platform_error') { + console.log((error as any).data?.error); + } +}); +``` + +**After (v5):** + +```typescript +import { App } from '@slack/bolt'; +import { WebAPIPlatformError, WebAPIRequestError } from '@slack/web-api'; + +const app = new App({ /* ... */ }); + +app.error(async ({ error }) => { + if (error instanceof WebAPIPlatformError) { + console.log(error.data.error); // e.g. 'channel_not_found' + } else if (error instanceof WebAPIRequestError) { + console.log(error.cause); // the underlying fetch/network error + } +}); +``` + +--- + +## New Features {#new-features} + +### We've added a `dispatcher` option to the `SocketModeReceiver` class {#new-dispatcher-option} + +Unified proxy and TLS configuration for both WebSocket connections and HTTP API calls. Pass any undici-compatible `Dispatcher` instance. See the section above on [the updated `SocketModeReceiver` class](#socketmodereceiver-dispatcher). + +### We've added `instanceof` operator support to error classes {#instanceof-errors} + +Both Bolt's own error classes (e.g., `AppInitializationError`, `AuthorizationError`) and the underlying `@slack/web-api` Node Slack SDK package error classes (e.g., `WebAPIPlatformError`, `WebAPIRequestError`) now properly extend the `Error` class. Use the `instanceof` operator for type-safe error handling instead of string comparisons on the `error.code` property. + +### We've made the `app.function()` method the sole custom step mechanism {#app-function} + +While the `app.function()` method already existed in Bolt v4, it is now the only way to handle custom function executions (replacing the removed `app.step()` method and `WorkflowStep` class). It provides `complete()` and `fail()` callback functions for signaling outcomes, and an `inputs` property for accessing function parameters. From a7e5fddbf53a2a7c4e49eaddf46bc8bbd9550b54 Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 16 Jun 2026 12:50:24 -0400 Subject: [PATCH 10/11] chore: add missing file --- src/context/create-respond.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/context/create-respond.ts b/src/context/create-respond.ts index febbff45d..69796c0d6 100644 --- a/src/context/create-respond.ts +++ b/src/context/create-respond.ts @@ -10,5 +10,14 @@ export function createRespond(fetchFn: FetchFunction, responseUrl: string): Resp headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(normalizedArgs), }); + // fetch resolves regardless of status code. + // Throw so that failures (e.g. expired response_url, rate limits) reach the app's error handling. + if (!response.ok) { + throw new RespondError( + `Failed to respond to the response_url: ${response.status} ${response.statusText}`, + response.status, + ); + } + return response; }; } From f9e887f606b97f3f36fd1ece952364ff5c8783ba Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Tue, 16 Jun 2026 16:38:47 -0400 Subject: [PATCH 11/11] chore: mention typescript bump in migration-v5.md --- docs/english/migration/migration-v5.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/english/migration/migration-v5.md b/docs/english/migration/migration-v5.md index 007ab6830..6125e5838 100644 --- a/docs/english/migration/migration-v5.md +++ b/docs/english/migration/migration-v5.md @@ -22,6 +22,22 @@ We've dropped support for Node.js 18. Node.js 20 or later is required. --- +### TypeScript consumers target ES2022 or later {#typescript-es2022-target} + +Bolt v5 depends on v8 of the `@slack/web-api` Node Slack SDK package, whose error classes use the ES2022 [`Error(message, { cause })`](https://developer.mozilla.org/en-US/docs/Web/API/Error/Error) constructor. Its type definitions reference the global `ErrorOptions` type. + +If your project's `tsconfig.json` targets an older ECMAScript library and does not set `skipLibCheck`, your build may fail after upgrading with an error like: + +```text +error TS2304: Cannot find name 'ErrorOptions'. +``` + +To fix it we recommend raising your compilation target to `es2022` or later, as it aligns with the Node.js 20 baseline. + +Alternatively, set `"skipLibCheck": true` to skip type-checking of dependency declaration files. Note that Bolt's own build and its sample apps already extend [`@tsconfig/node20`](https://www.npmjs.com/package/@tsconfig/node20), which targets `es2022`, so projects based on those templates are unaffected. + +--- + ### We've removed the `agent` and `clientTls` options from the `AppOptions` interface {#removed-agent-clienttls} You should configure transport via the `clientOptions.fetch` option or use the Node.js built-in proxy support.