From 43050e0a9ec94d6d42a007d656b4f3ffb0d8ed2f Mon Sep 17 00:00:00 2001 From: William Bergamin Date: Thu, 14 May 2026 18:30:58 -0400 Subject: [PATCH 1/7] 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 2/7] 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 3/7] 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 4/7] 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 5/7] 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 6/7] 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 7/7] 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'); + } + }); +});