Repository: slackapi/bolt-js
Pull request #2128 (2024-06-06 02:50:16)

Fixes and polish for stable release
###  Summary

Final preparations and polish prior to cutting a stable release. 💅🏼 

- Makes `inputs` available by way of `Context` for interactivity events that are associated with a function's execution
- Added three function-related props to the `Context` interface
- Surface proper errors if the `functionExecutionId` somehow goes missing when using a utility
- Incorporates base-branch (`feat-functions`) [PR feedback](https://github.com/slackapi/bolt-js/pull/2026/files) (TS fixes)
- **TODO**: TS fix for `action` payload

### Requirements (place an `x` in each `[ ]`)

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/bolt/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) Report
Attention: Patch coverage is `35.00000%` with `13 lines` in your changes missing coverage. Please review.
> Project coverage is 80.90%. Comparing base [(`e933eda`)](https://app.codecov.io/gh/slackapi/bolt-js/commit/e933edaafd70c4d791bd01e7b07ee9d8b7b6a32e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) to head [(`c1a9127`)](https://app.codecov.io/gh/slackapi/bolt-js/commit/c1a91271f16a68d73a5fc2c563cc2ea65a556355?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).

| [Files](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) | Patch % | Lines |
|---|---|---|
| [src/CustomFunction.ts](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&filepath=src%2FCustomFunction.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi#diff-c3JjL0N1c3RvbUZ1bmN0aW9uLnRz) | 14.28% | [5 Missing and 1 partial :warning: ](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) |
| [src/App.ts](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&filepath=src%2FApp.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi#diff-c3JjL0FwcC50cw==) | 28.57% | [4 Missing and 1 partial :warning: ](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) |
| [src/errors.ts](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&filepath=src%2Ferrors.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi#diff-c3JjL2Vycm9ycy50cw==) | 66.66% | [2 Missing :warning: ](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi) |

<details><summary>Additional details and impacted files</summary>


```diff
@@                Coverage Diff                 @@
##           feat-functions    #2128      +/-   ##
==================================================
- Coverage           81.45%   80.90%   -0.56%     
==================================================
  Files                  19       19              
  Lines                1618     1634      +16     
  Branches              458      461       +3     
==================================================
+ Hits                 1318     1322       +4     
- Misses                193      204      +11     
- Partials              107      108       +1     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/slackapi/bolt-js/pull/2128?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=slackapi).


Patch:
diff --git a/package.json b/package.json
index 2ac6247..40c1805 100644
--- a/package.json
+++ b/package.json
@@ -30,7 +30,8 @@
     "build:clean": "shx rm -rf ./dist ./coverage ./.nyc_output",
     "lint": "eslint --fix --ext .ts src",
     "mocha": "TS_NODE_PROJECT=tsconfig.json nyc mocha --config .mocharc.json \"src/**/*.spec.ts\"",
-    "test": "npm run lint && npm run mocha && npm run test:types",
+    "test": "npm run build && npm run lint && npm run mocha && npm run test:types",
+    "test:coverage": "npm run mocha && nyc report --reporter=text",
     "test:types": "tsd",
     "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
   },
diff --git a/src/App.ts b/src/App.ts
index 9902de8..ac4cbef 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -54,6 +54,7 @@ import {
   SlashCommand,
   WorkflowStepEdit,
   SlackOptions,
+  FunctionInputs,
 } from './types';
 import { IncomingEventType, getTypeAndConversation, assertNever, isBodyWithTypeEnterpriseInstall, isEventTypeToSkipAuthorize } from './helpers';
 import { CodedError, asCodedError, AppInitializationError, MultipleListenerError, ErrorCode, InvalidCustomPropertyError } from './errors';
@@ -502,6 +503,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     }
   }
 
+  public get webClientOptions(): WebClientOptions {
+    return this.clientOptions;
+  }
+
   /**
    * Register a new middleware, processed in the order registered.
    *
@@ -529,7 +534,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
  * Register CustomFunction middleware
  */
   public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this {
-    const fn = new CustomFunction(callbackId, listeners);
+    const fn = new CustomFunction(callbackId, listeners, this.webClientOptions);
     const m = fn.getMiddleware();
     this.middleware.push(m);
     return this;
@@ -964,9 +969,12 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       retryReason: event.retryReason,
     };
 
-    // Extract function-related information and augment to context
-    const { functionExecutionId, functionBotAccessToken } = extractFunctionContext(body);
-    if (functionExecutionId) { context.functionExecutionId = functionExecutionId; }
+    // Extract function-related information and augment context
+    const { functionExecutionId, functionBotAccessToken, functionInputs } = extractFunctionContext(body);
+    if (functionExecutionId) {
+      context.functionExecutionId = functionExecutionId;
+      if (functionInputs) { context.functionInputs = functionInputs; }
+    }
 
     if (this.attachFunctionToken) {
       if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
@@ -1029,6 +1037,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       ack?: AckFn<any>;
       complete?: FunctionCompleteFn;
       fail?: FunctionFailFn;
+      inputs?: FunctionInputs;
     } = {
       body: bodyArg,
       payload,
@@ -1088,6 +1097,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
       listenerArgs.complete = CustomFunction.createFunctionComplete(context, client);
       listenerArgs.fail = CustomFunction.createFunctionFail(context, client);
+      listenerArgs.inputs = context.functionInputs;
     }
 
     if (token !== undefined) {
@@ -1599,6 +1609,7 @@ function escapeHtml(input: string | undefined | null): string {
 function extractFunctionContext(body: StringIndexed) {
   let functionExecutionId;
   let functionBotAccessToken;
+  let functionInputs;
 
   // function_executed event
   if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
@@ -1610,9 +1621,10 @@ function extractFunctionContext(body: StringIndexed) {
   if (body.function_data) {
     functionExecutionId = body.function_data.execution_id;
     functionBotAccessToken = body.bot_access_token;
+    functionInputs = body.function_data.inputs;
   }
 
-  return { functionExecutionId, functionBotAccessToken };
+  return { functionExecutionId, functionBotAccessToken, functionInputs };
 }
 
 // ----------------------------
diff --git a/src/CustomFunction.spec.ts b/src/CustomFunction.spec.ts
index 5b5a542..24dc000 100644
--- a/src/CustomFunction.spec.ts
+++ b/src/CustomFunction.spec.ts
@@ -2,6 +2,7 @@ import 'mocha';
 import { assert } from 'chai';
 import sinon from 'sinon';
 import rewiremock from 'rewiremock';
+import { WebClient } from '@slack/web-api';
 import {
   CustomFunction,
   SlackCustomFunctionMiddlewareArgs,
@@ -9,8 +10,8 @@ import {
   CustomFunctionMiddleware,
   CustomFunctionExecuteMiddlewareArgs,
 } from './CustomFunction';
-import { Override } from './test-helpers';
-import { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware } from './types';
+import { createFakeLogger, Override } from './test-helpers';
+import { AllMiddlewareArgs, Middleware } from './types';
 import { CustomFunctionInitializationError } from './errors';
 
 async function importCustomFunction(overrides: Override = {}): Promise<typeof import('./CustomFunction')> {
@@ -26,36 +27,35 @@ const MOCK_MIDDLEWARE_MULTIPLE = [MOCK_FN, MOCK_FN_2];
 describe('CustomFunction class', () => {
   describe('constructor', () => {
     it('should accept single function as middleware', async () => {
-      const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE);
+      const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
       assert.isNotNull(fn);
     });
 
     it('should accept multiple functions as middleware', async () => {
-      const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE);
+      const fn = new CustomFunction('test_callback_id', MOCK_MIDDLEWARE_MULTIPLE, {});
       assert.isNotNull(fn);
     });
   });
 
   describe('getMiddleware', () => {
     it('should not call next if a function_executed event', async () => {
-      const fn = new CustomFunction('test_executed_callback_id', MOCK_MIDDLEWARE_SINGLE);
+      const cbId = 'test_executed_callback_id';
+      const fn = new CustomFunction(cbId, MOCK_MIDDLEWARE_SINGLE, {});
       const middleware = fn.getMiddleware();
-      const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
-        SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
+      const fakeEditArgs = createFakeFunctionExecutedEvent(cbId);
 
       const fakeNext = sinon.spy();
       fakeEditArgs.next = fakeNext;
 
       await middleware(fakeEditArgs);
 
-      assert(fakeNext.notCalled);
+      assert(fakeNext.notCalled, 'next called!');
     });
 
     it('should call next if valid custom function but mismatched callback_id', async () => {
-      const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE);
+      const fn = new CustomFunction('bad_executed_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
       const middleware = fn.getMiddleware();
-      const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
-        SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
+      const fakeEditArgs = createFakeFunctionExecutedEvent();
 
       const fakeNext = sinon.spy();
       fakeEditArgs.next = fakeNext;
@@ -65,8 +65,8 @@ describe('CustomFunction class', () => {
       assert(fakeNext.called);
     });
 
-    it('should call next if not a workflow step event', async () => {
-      const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE);
+    it('should call next if not a function executed event', async () => {
+      const fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE, {});
       const middleware = fn.getMiddleware();
       const fakeViewArgs = createFakeViewEvent() as unknown as
         SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
@@ -120,8 +120,7 @@ describe('CustomFunction class', () => {
 
   describe('isFunctionEvent', () => {
     it('should return true if recognized function_executed payload type', async () => {
-      const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as SlackCustomFunctionMiddlewareArgs
-      & AllMiddlewareArgs;
+      const fakeExecuteArgs = createFakeFunctionExecutedEvent();
 
       const { isFunctionEvent } = await importCustomFunction();
       const eventIsFunctionExcuted = isFunctionEvent(fakeExecuteArgs);
@@ -130,7 +129,8 @@ describe('CustomFunction class', () => {
     });
 
     it('should return false if not a function_executed payload type', async () => {
-      const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
+      const fakeExecutedEvent = createFakeFunctionExecutedEvent();
+      // @ts-expect-error expected invalid payload type
       fakeExecutedEvent.payload.type = 'invalid_type';
 
       const { isFunctionEvent } = await importCustomFunction();
@@ -142,10 +142,10 @@ describe('CustomFunction class', () => {
 
   describe('enrichFunctionArgs', () => {
     it('should remove next() from all original event args', async () => {
-      const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
+      const fakeExecutedEvent = createFakeFunctionExecutedEvent();
 
       const { enrichFunctionArgs } = await importCustomFunction();
-      const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent);
+      const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent, {});
 
       assert.notExists(executeFunctionArgs.next);
     });
@@ -154,7 +154,7 @@ describe('CustomFunction class', () => {
       const fakeArgs = createFakeFunctionExecutedEvent();
 
       const { enrichFunctionArgs } = await importCustomFunction();
-      const functionArgs = enrichFunctionArgs(fakeArgs);
+      const functionArgs = enrichFunctionArgs(fakeArgs, {});
 
       assert.exists(functionArgs.inputs);
       assert.exists(functionArgs.complete);
@@ -163,19 +163,43 @@ describe('CustomFunction class', () => {
   });
 
   describe('custom function utility functions', () => {
-    it('complete should call functions.completeSuccess', async () => {
-      // TODO
+    describe('`complete` factory function', () => {
+      it('complete should call functions.completeSuccess', async () => {
+        const client = new WebClient('sometoken');
+        const completeMock = sinon.stub(client.functions, 'completeSuccess').resolves();
+        const complete = CustomFunction.createFunctionComplete({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client);
+        await complete();
+        assert(completeMock.called, 'client.functions.completeSuccess not called!');
+      });
+      it('should throw if no functionExecutionId present on context', () => {
+        const client = new WebClient('sometoken');
+        assert.throws(() => {
+          CustomFunction.createFunctionComplete({ isEnterpriseInstall: false }, client);
+        });
+      });
     });
 
-    it('fail should call functions.completeError', async () => {
-      // TODO
+    describe('`fail` factory function', () => {
+      it('fail should call functions.completeError', async () => {
+        const client = new WebClient('sometoken');
+        const completeMock = sinon.stub(client.functions, 'completeError').resolves();
+        const complete = CustomFunction.createFunctionFail({ isEnterpriseInstall: false, functionExecutionId: 'Fx1234' }, client);
+        await complete({ error: 'boom' });
+        assert(completeMock.called, 'client.functions.completeError not called!');
+      });
+      it('should throw if no functionExecutionId present on context', () => {
+        const client = new WebClient('sometoken');
+        assert.throws(() => {
+          CustomFunction.createFunctionFail({ isEnterpriseInstall: false }, client);
+        });
+      });
     });
 
     it('inputs should map to function payload inputs', async () => {
-      const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
+      const fakeExecuteArgs = createFakeFunctionExecutedEvent();
 
       const { enrichFunctionArgs } = await importCustomFunction();
-      const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs);
+      const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs, {});
 
       assert.isTrue(enrichedArgs.inputs === fakeExecuteArgs.event.inputs);
     });
@@ -183,39 +207,79 @@ describe('CustomFunction class', () => {
 
   describe('processFunctionMiddleware', () => {
     it('should call each callback in user-provided middleware', async () => {
-      const { ...fakeArgs } = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
+      const { ...fakeArgs } = createFakeFunctionExecutedEvent();
       const { processFunctionMiddleware } = await importCustomFunction();
 
       const fn1 = sinon.spy((async ({ next: continuation }) => {
         await continuation();
       }) as Middleware<CustomFunctionExecuteMiddlewareArgs>);
-      const fn2 = sinon.spy(async () => {});
+      const fn2 = sinon.spy(async () => {
+      });
       const fakeMiddleware = [fn1, fn2] as CustomFunctionMiddleware;
 
       await processFunctionMiddleware(fakeArgs, fakeMiddleware);
 
-      assert(fn1.called);
-      assert(fn2.called);
+      assert(fn1.called, 'first user-provided middleware not called!');
+      assert(fn2.called, 'second user-provided middleware not called!');
     });
   });
 });
 
-function createFakeFunctionExecutedEvent() {
+function createFakeFunctionExecutedEvent(callbackId?: string): AllCustomFunctionMiddlewareArgs {
+  const func = {
+    type: 'function',
+    id: 'somefunc',
+    callback_id: callbackId || 'callback_id',
+    title: 'My dope function',
+    input_parameters: [],
+    output_parameters: [],
+    app_id: 'A1234',
+    date_created: 123456,
+    date_deleted: 0,
+    date_updated: 123456,
+  };
+  const base = {
+    bot_access_token: 'xoxb-abcd-1234',
+    event_ts: '123456.789',
+    function_execution_id: 'Fx1234',
+    workflow_execution_id: 'Wf1234',
+    type: 'function_executed',
+  } as const;
+  const inputs = { message: 'test123', recipient: 'U012345' };
+  const event = {
+    function: func,
+    inputs,
+    ...base,
+  } as const;
   return {
-    event: {
-      inputs: { message: 'test123', recipient: 'U012345' },
-    },
-    payload: {
-      type: 'function_executed',
-      function: {
-        callback_id: 'test_executed_callback_id',
-      },
-      inputs: { message: 'test123', recipient: 'U012345' },
-      bot_access_token: 'xwfp-123',
+    body: {
+      api_app_id: 'A1234',
+      event,
+      event_id: 'E1234',
+      event_time: 123456,
+      team_id: 'T1234',
+      token: 'xoxb-1234',
+      type: 'event_callback',
     },
+    client: new WebClient('faketoken'),
+    complete: () => Promise.resolve({ ok: true }),
     context: {
       functionBotAccessToken: 'xwfp-123',
+      functionExecutionId: 'test_executed_callback_id',
+      isEnterpriseInstall: false,
+    },
+    event,
+    fail: () => Promise.resolve({ ok: true }),
+    inputs,
+    logger: createFakeLogger(),
+    message: undefined,
+    next: () => Promise.resolve(),
+    payload: {
+      function: func,
+      inputs: { message: 'test123', recipient: 'U012345' },
+      ...base,
     },
+    say: undefined,
   };
 }
 
diff --git a/src/CustomFunction.ts b/src/CustomFunction.ts
index 8f52c7e..4033bc0 100644
--- a/src/CustomFunction.ts
+++ b/src/CustomFunction.ts
@@ -3,6 +3,7 @@ import {
   WebClient,
   FunctionsCompleteErrorResponse,
   FunctionsCompleteSuccessResponse,
+  WebClientOptions,
 } from '@slack/web-api';
 import {
   Middleware,
@@ -13,7 +14,7 @@ import {
   FunctionExecutedEvent,
 } from './types';
 import processMiddleware from './middleware/process';
-import { CustomFunctionInitializationError } from './errors';
+import { CustomFunctionCompleteFailError, CustomFunctionCompleteSuccessError, CustomFunctionInitializationError } from './errors';
 
 /** Interfaces */
 
@@ -62,14 +63,18 @@ export class CustomFunction {
   /** Function callback_id */
   public callbackId: string;
 
+  private appWebClientOptions: WebClientOptions;
+
   private middleware: CustomFunctionMiddleware;
 
   public constructor(
     callbackId: string,
     middleware: CustomFunctionExecuteMiddleware,
+    clientOptions: WebClientOptions,
   ) {
     validate(callbackId, middleware);
 
+    this.appWebClientOptions = clientOptions;
     this.callbackId = callbackId;
     this.middleware = middleware;
   }
@@ -88,7 +93,7 @@ export class CustomFunction {
   }
 
   private async processEvent(args: AllCustomFunctionMiddlewareArgs): Promise<void> {
-    const functionArgs = enrichFunctionArgs(args);
+    const functionArgs = enrichFunctionArgs(args, this.appWebClientOptions);
     const functionMiddleware = this.getFunctionMiddleware();
     return processFunctionMiddleware(functionArgs, functionMiddleware);
   }
@@ -99,12 +104,16 @@ export class CustomFunction {
 
   /**
    * Factory for `complete()` utility
-   * @param args function_executed event
    */
   public static createFunctionComplete(context: Context, client: WebClient): FunctionCompleteFn {
     const token = selectToken(context);
     const { functionExecutionId } = context;
 
+    if (!functionExecutionId) {
+      const errorMsg = 'No function_execution_id found';
+      throw new CustomFunctionCompleteSuccessError(errorMsg);
+    }
+
     return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
       token,
       outputs: params.outputs || {},
@@ -114,14 +123,18 @@ export class CustomFunction {
 
   /**
  * Factory for `fail()` utility
- * @param args function_executed event
  */
   public static createFunctionFail(context: Context, client: WebClient): FunctionFailFn {
     const token = selectToken(context);
+    const { functionExecutionId } = context;
+
+    if (!functionExecutionId) {
+      const errorMsg = 'No function_execution_id found';
+      throw new CustomFunctionCompleteFailError(errorMsg);
+    }
 
     return (params: Parameters<FunctionFailFn>[0]) => {
       const { error } = params ?? {};
-      const { functionExecutionId } = context;
 
       return client.functions.completeError({
         token,
@@ -158,8 +171,7 @@ export function validate(callbackId: string, middleware: CustomFunctionExecuteMi
 }
 
 /**
- * `processFunctionMiddleware()` invokes each callback for lifecycle event
- * @param args workflow_step_edit action
+ * `processFunctionMiddleware()` invokes each listener middleware
  */
 export async function processFunctionMiddleware(
   args: AllCustomFunctionMiddlewareArgs,
@@ -192,14 +204,16 @@ function selectToken(context: Context): string | undefined {
  *    - events will *not* continue down global middleware chain to subsequent listeners
  *  2. augments args with step lifecycle-specific properties/utilities
  * */
-export function enrichFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs {
+export function enrichFunctionArgs(
+  args: AllCustomFunctionMiddlewareArgs, webClientOptions: WebClientOptions,
+): AllCustomFunctionMiddlewareArgs {
   const { next: _next, ...functionArgs } = args;
-  const enrichedArgs: any = { ...functionArgs };
+  const enrichedArgs = { ...functionArgs };
   const token = selectToken(functionArgs.context);
 
   // Making calls with a functionBotAccessToken establishes continuity between
   // a function_executed event and subsequent interactive events (actions)
-  const client = new WebClient(token, { ...functionArgs.client });
+  const client = new WebClient(token, webClientOptions);
   enrichedArgs.client = client;
 
   // Utility args
@@ -207,5 +221,5 @@ export function enrichFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs {
   enrichedArgs.complete = CustomFunction.createFunctionComplete(enrichedArgs.context, client);
   enrichedArgs.fail = CustomFunction.createFunctionFail(enrichedArgs.context, client);
 
-  return enrichedArgs;
+  return enrichedArgs as AllCustomFunctionMiddlewareArgs; // TODO: dangerous casting as it obfuscates missing `next()`
 }
diff --git a/src/errors.ts b/src/errors.ts
index c270b9c..09a5a8b 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -41,6 +41,8 @@ export enum ErrorCode {
   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',
 }
 
 export class UnknownError extends Error implements CodedError {
@@ -149,3 +151,11 @@ export class WorkflowStepInitializationError extends Error implements CodedError
 export class CustomFunctionInitializationError extends Error implements CodedError {
   public code = ErrorCode.CustomFunctionInitializationError;
 }
+
+export class CustomFunctionCompleteSuccessError extends Error implements CodedError {
+  public code = ErrorCode.CustomFunctionCompleteSuccessError;
+}
+
+export class CustomFunctionCompleteFailError extends Error implements CodedError {
+  public code = ErrorCode.CustomFunctionCompleteFailError;
+}
diff --git a/src/middleware/builtin.ts b/src/middleware/builtin.ts
index 69f908c..6152c3a 100644
--- a/src/middleware/builtin.ts
+++ b/src/middleware/builtin.ts
@@ -299,15 +299,19 @@ export function ignoreSelf(): Middleware<AnyMiddlewareArgs> {
     const botUserId = args.context.botUserId !== undefined ? (args.context.botUserId as string) : undefined;
 
     if (isEventArgs(args)) {
-      // Once we've narrowed the type down to SlackEventMiddlewareArgs, there's no way to further narrow it down to
-      // SlackEventMiddlewareArgs<'message'> without a cast, so the following couple lines do that.
-      if (args.message !== undefined) {
-        const message = args.message as SlackEventMiddlewareArgs<'message'>['message'];
+      if (args.event.type === 'message') {
+        // Once we've narrowed the type down to SlackEventMiddlewareArgs, there's no way to further narrow it down to
+        // SlackEventMiddlewareArgs<'message'> without a cast, so the following couple lines do that.
+        // TODO: there must be a better way; generics-based types for event and middleware arguments likely the issue
+        // should instead use a discriminated union
+        const message = args.message as unknown as SlackEventMiddlewareArgs<'message'>['message'];
+        if (message !== undefined) {
         // TODO: revisit this once we have all the message subtypes defined to see if we can do this better with
         // type narrowing
         // Look for an event that is identified as a bot message from the same bot ID as this app, and return to skip
-        if (message.subtype === 'bot_message' && message.bot_id === botId) {
-          return;
+          if (message.subtype === 'bot_message' && message.bot_id === botId) {
+            return;
+          }
         }
       }
 
@@ -331,7 +335,7 @@ export function ignoreSelf(): Middleware<AnyMiddlewareArgs> {
  */
 export function subtype(subtype1: string): Middleware<SlackEventMiddlewareArgs<'message'>> {
   return async ({ message, next }) => {
-    if (message.subtype === subtype1) {
+    if (message && message.subtype === subtype1) {
       await next();
     }
   };
@@ -354,7 +358,7 @@ export function directMention(): Middleware<SlackEventMiddlewareArgs<'message'>>
       );
     }
 
-    if (!('text' in message) || message.text === undefined) {
+    if (!message || !('text' in message) || message.text === undefined) {
       return;
     }
 
@@ -406,6 +410,8 @@ function isViewBody(
   return (body as SlackViewAction).view !== undefined;
 }
 
-function isEventArgs(args: AnyMiddlewareArgs): args is SlackEventMiddlewareArgs {
+function isEventArgs(
+  args: AnyMiddlewareArgs,
+): args is SlackEventMiddlewareArgs {
   return (args as SlackEventMiddlewareArgs).event !== undefined;
 }
diff --git a/src/middleware/process.ts b/src/middleware/process.ts
index b183f46..cc963d8 100644
--- a/src/middleware/process.ts
+++ b/src/middleware/process.ts
@@ -21,8 +21,8 @@ export default async function processMiddleware(
     if (toCallMiddlewareIndex < middleware.length) {
       lastCalledMiddlewareIndex = toCallMiddlewareIndex;
       return middleware[toCallMiddlewareIndex]({
-        next: () => invokeMiddleware(toCallMiddlewareIndex + 1),
         ...initialArgs,
+        next: () => invokeMiddleware(toCallMiddlewareIndex + 1),
         context,
         client,
         logger,
diff --git a/src/types/actions/index.ts b/src/types/actions/index.ts
index c94a867..5c7e784 100644
--- a/src/types/actions/index.ts
+++ b/src/types/actions/index.ts
@@ -3,6 +3,8 @@ import { InteractiveMessage } from './interactive-message';
 import { WorkflowStepEdit } from './workflow-step-edit';
 import { DialogSubmitAction, DialogValidation } from './dialog-action';
 import { SayFn, SayArguments, RespondFn, AckFn } from '../utilities';
+import { FunctionCompleteFn, FunctionFailFn } from '../../CustomFunction';
+import { FunctionInputs } from '../events';
 
 export * from './block-action';
 export * from './interactive-message';
@@ -42,9 +44,12 @@ export interface SlackActionMiddlewareArgs<Action extends SlackAction = SlackAct
   action: this['payload'];
   body: Action;
   // all action types except dialog submission have a channel context
-  say: Action extends Exclude<SlackAction, DialogSubmitAction | WorkflowStepEdit> ? SayFn : never;
+  say: Action extends Exclude<SlackAction, DialogSubmitAction | WorkflowStepEdit> ? SayFn : undefined;
   respond: RespondFn;
   ack: ActionAckFn<Action>;
+  complete?: FunctionCompleteFn;
+  fail?: FunctionFailFn;
+  inputs?: FunctionInputs;
 }
 
 /**
diff --git a/src/types/events/base-events.ts b/src/types/events/base-events.ts
index 32d9ae5..9abc995 100644
--- a/src/types/events/base-events.ts
+++ b/src/types/events/base-events.ts
@@ -432,11 +432,11 @@ export interface FileUnsharedEvent {
 }
 
 export interface FunctionParams {
-  type?: string;
-  name?: string;
+  type: string;
+  name: string;
   description?: string;
   title?: string;
-  is_required?: boolean;
+  is_required: boolean;
 }
 
 export interface FunctionInputs {
@@ -451,7 +451,7 @@ export interface FunctionExecutedEvent {
     id: string;
     callback_id: string;
     title: string;
-    description: string;
+    description?: string;
     type: string;
     input_parameters: FunctionParams[];
     output_parameters: FunctionParams[];
diff --git a/src/types/events/index.ts b/src/types/events/index.ts
index b888b4a..0b2c0ef 100644
--- a/src/types/events/index.ts
+++ b/src/types/events/index.ts
@@ -29,11 +29,11 @@ export {
 export interface SlackEventMiddlewareArgs<EventType extends string = string> {
   payload: EventFromType<EventType>;
   event: this['payload'];
-  message: EventType extends 'message' ? this['payload'] : never;
+  message: EventType extends 'message' ? this['payload'] : undefined;
   body: EnvelopedEvent<this['payload']>;
   say: WhenEventHasChannelContext<this['payload'], SayFn>;
   // Add `ack` as undefined for global middleware in TypeScript
-  ack: undefined;
+  ack?: undefined;
 }
 
 /**
@@ -78,8 +78,8 @@ export type KnownEventFromType<T extends string> = Extract<SlackEvent, { type: T
 
 /**
  * Type function which tests whether or not the given `Event` contains a channel ID context for where the event
- * occurred, and returns `Type` when the test passes. Otherwise this returns `never`.
+ * occurred, and returns `Type` when the test passes. Otherwise this returns `undefined`.
  */
 type WhenEventHasChannelContext<Event, Type> = Event extends { channel: string } | { item: { channel: string } }
   ? Type
-  : never;
+  : undefined;
diff --git a/src/types/helpers.ts b/src/types/helpers.ts
index 3a7a8bb..124f842 100644
--- a/src/types/helpers.ts
+++ b/src/types/helpers.ts
@@ -1,9 +1,11 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
+// TODO: breaking change: remove, unnecessary abstraction, just use Record directly
 /**
  * Extend this interface to build a type that is treated as an open set of properties, where each key is a string.
  */
 export type StringIndexed = Record<string, any>;
 
+// TODO: breaking change: no longer used! remove
 /**
  * @deprecated No longer works in TypeScript 4.3
  */
diff --git a/src/types/middleware.ts b/src/types/middleware.ts
index a36fddc..d1d8878 100644
--- a/src/types/middleware.ts
+++ b/src/types/middleware.ts
@@ -1,7 +1,7 @@
 import { WebClient } from '@slack/web-api';
 import { Logger } from '@slack/logger';
 import { StringIndexed } from './helpers';
-import { SlackEventMiddlewareArgs } from './events';
+import { FunctionInputs, SlackEventMiddlewareArgs } from './events';
 import { SlackActionMiddlewareArgs } from './actions';
 import { SlackCommandMiddlewareArgs } from './command';
 import { SlackOptionsMiddlewareArgs } from './options';
@@ -73,6 +73,23 @@ export interface Context extends StringIndexed {
    */
   isEnterpriseInstall: boolean,
 
+  /**
+   * A JIT and function-specific token that, when used to make API calls,
+   * creates an association between a function's execution and subsequent actions
+   * (e.g., buttons and other interactivity)
+   */
+  functionBotAccessToken?: string;
+
+  /**
+   * Function execution ID associated with the event
+   */
+  functionExecutionId?: string;
+
+  /**
+   * Inputs that were provided to a function when it was executed
+   */
+  functionInputs?: FunctionInputs;
+
   /**
    * Retry count of an Events API request (this property does not exist for other requests)
    */
@@ -90,6 +107,9 @@ export const contextBuiltinKeys: string[] = [
   'botUserId',
   'teamId',
   'enterpriseId',
+  'functionBotAccessToken',
+  'functionExecutionId',
+  'functionInputs',
   'retryNum',
   'retryReason',
 ];
diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json
index 776c2cf..d554c4b 100644
--- a/tsconfig.eslint.json
+++ b/tsconfig.eslint.json
@@ -14,6 +14,7 @@
     ".eslintrc.js",
     "docs/**/*",
     "examples/**/*",
+    "types-tests/**/*"
   ],
   "exclude": [
     // Overwrite exclude from the base config to clear the value
@@ -21,6 +22,5 @@
     // Contains external module type definitions, which are not subject to this project's style rules
     "types/**/*",
     // Contain intentional type checking issues for the purpose of testing the typechecker's output
-    "types-tests/**/*"
   ]
 }


Test patch:
diff --git a/types-tests/event.test-d.ts b/types-tests/event.test-d.ts
index b24a668..06a27ab 100644
--- a/types-tests/event.test-d.ts
+++ b/types-tests/event.test-d.ts
@@ -8,7 +8,7 @@ expectType<void>(
     expectType<AppMentionEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -16,7 +16,7 @@ expectType<void>(
     expectType<ReactionAddedEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -24,7 +24,7 @@ expectType<void>(
     expectType<ReactionRemovedEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -32,7 +32,7 @@ expectType<void>(
     expectType<UserHuddleChangedEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -40,7 +40,7 @@ expectType<void>(
     expectType<UserProfileChangedEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -48,7 +48,7 @@ expectType<void>(
     expectType<UserStatusChangedEvent>(event);
     expectNotType<SlackEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -56,7 +56,7 @@ expectType<void>(
     expectType<SayFn>(say);
     expectType<PinAddedEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
@@ -64,19 +64,19 @@ expectType<void>(
     expectType<SayFn>(say);
     expectType<PinRemovedEvent>(event);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
   app.event('reaction_added', async ({ say, event }) => {
     expectType<SayFn>(say);
     await Promise.resolve(event);
-  })
+  }),
 );
 
 expectType<void>(
   app.event('reaction_removed', async ({ say, event }) => {
     expectType<SayFn>(say);
     await Promise.resolve(event);
-  })
+  }),
 );