From a050256c7e08f3508b15f2d1a839758da0678a9c Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Wed, 8 Nov 2023 11:32:47 -0800
Subject: [PATCH 01/13] [WIP] Add function listener support

---
 src/App.ts                      |  11 ++
 src/WorkflowFunction.ts         | 217 ++++++++++++++++++++++++++++++++
 src/errors.ts                   |   6 +
 src/types/events/base-events.ts |  35 ++++++
 4 files changed, 269 insertions(+)
 create mode 100644 src/WorkflowFunction.ts

diff --git a/src/App.ts b/src/App.ts
index 5fea2290c..906b6d66a 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -61,6 +61,7 @@ import { AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
 import { StringIndexed } from './types/helpers';
 // eslint-disable-next-line import/order
 import allSettled = require('promise.allsettled'); // eslint-disable-line @typescript-eslint/no-require-imports
+import { WorkflowFunction, WorkflowFunctionMiddleware } from './WorkflowFunction';
 // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs
 const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
 
@@ -517,6 +518,16 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     return this;
   }
 
+  /**
+ * Register WorkflowFunction middleware
+ */
+  public function(callbackId: string, ...listeners: WorkflowFunctionMiddleware): this {
+    const fn = new WorkflowFunction(callbackId, listeners);
+    const m = fn.getMiddleware();
+    this.middleware.push(m);
+    return this;
+  }
+
   /**
    * Convenience method to call start on the receiver
    *
diff --git a/src/WorkflowFunction.ts b/src/WorkflowFunction.ts
new file mode 100644
index 000000000..23609f0d6
--- /dev/null
+++ b/src/WorkflowFunction.ts
@@ -0,0 +1,217 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  WorkflowsStepCompletedResponse,
+  WorkflowsStepFailedResponse,
+  // FunctionCompleteErrorResponse,
+  // FunctionCompleteSuccessResponse,
+} from '@slack/web-api';
+import {
+  Middleware,
+  AllMiddlewareArgs,
+  AnyMiddlewareArgs,
+  SlackEventMiddlewareArgs,
+  FunctionExecutedEvent,
+} from './types';
+import processMiddleware from './middleware/process';
+import { WorkflowFunctionInitializationError } from './errors';
+
+/** Interfaces */
+
+export interface FunctionCompleteArguments {
+  outputs?: {
+    [key: string]: any;
+  };
+}
+
+export interface FunctionFailArguments {
+  error: string;
+}
+
+export interface FunctionCompleteFn {
+  // TODO :: import FunctionCompleteErrorResponse from @slack/web-api
+  (params?: FunctionCompleteArguments): Promise<WorkflowsStepCompletedResponse>;
+}
+
+export interface FunctionFailFn {
+  // TODO :: import FunctionCompleteErrorResponse from @slack/web-api
+  (params: FunctionFailArguments): Promise<WorkflowsStepFailedResponse>;
+}
+
+export interface WorkflowFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
+  function: FunctionExecutedEvent;
+  complete: FunctionCompleteFn;
+  fail: FunctionFailFn;
+}
+
+/** Types */
+
+export type SlackWorkflowFunctionMiddlewareArgs = WorkflowFunctionExecuteMiddlewareArgs;
+
+export type WorkflowFunctionExecuteMiddleware = Middleware<WorkflowFunctionExecuteMiddlewareArgs>;
+
+export type WorkflowFunctionMiddleware = WorkflowFunctionExecuteMiddleware[];
+
+export type AllWorkflowFunctionMiddlewareArgs
+  <T extends SlackWorkflowFunctionMiddlewareArgs = SlackWorkflowFunctionMiddlewareArgs> = T & AllMiddlewareArgs;
+
+/** Constants */
+
+const VALID_PAYLOAD_TYPES = new Set(['function_executed']);
+
+/** Class */
+
+export class WorkflowFunction {
+  /** Function callback_id */
+  public callbackId: string;
+
+  /** Function definition */
+  private middleware: WorkflowFunctionMiddleware;
+
+  public constructor(
+    callbackId: string,
+    middleware: WorkflowFunctionMiddleware,
+  ) {
+    validate(callbackId, middleware);
+
+    this.callbackId = callbackId;
+    this.middleware = middleware;
+  }
+
+  public getMiddleware(): Middleware<AnyMiddlewareArgs> {
+    return async (args): Promise<any> => {
+      if (isFunctionEvent(args) && this.matchesConstraints(args)) {
+        return this.processEvent(args);
+      }
+      return args.next();
+    };
+  }
+
+  private matchesConstraints(args: SlackWorkflowFunctionMiddlewareArgs): boolean {
+    return args.payload.function.callback_id === this.callbackId;
+  }
+
+  private async processEvent(args: AllWorkflowFunctionMiddlewareArgs): Promise<void> {
+    const functionArgs = prepareFunctionArgs(args);
+    const stepMiddleware = this.getStepMiddleware();
+    return processStepMiddleware(functionArgs, stepMiddleware);
+  }
+
+  private getStepMiddleware(): WorkflowFunctionMiddleware {
+    return this.middleware;
+  }
+}
+
+/** Helper Functions */
+
+export function validate(callbackId: string, listeners: WorkflowFunctionMiddleware): void {
+  // Ensure callbackId is valid
+  if (typeof callbackId !== 'string') {
+    const errorMsg = 'WorkflowFunction expects a callback_id as the first argument';
+    throw new WorkflowFunctionInitializationError(errorMsg);
+  }
+
+  // TODO: Validate that all array members are functions
+  console.log(listeners);
+}
+
+/**
+ * `processStepMiddleware()` invokes each callback for lifecycle event
+ * @param args workflow_step_edit action
+ */
+export async function processStepMiddleware(
+  args: AllWorkflowFunctionMiddlewareArgs,
+  middleware: WorkflowFunctionMiddleware,
+): Promise<void> {
+  const { context, client, logger } = args;
+  const callbacks = [...middleware] as Middleware<AnyMiddlewareArgs>[];
+  const lastCallback = callbacks.pop();
+
+  if (lastCallback !== undefined) {
+    await processMiddleware(
+      callbacks, args, context, client, logger,
+      async () => lastCallback({ ...args, context, client, logger }),
+    );
+  }
+}
+
+export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllWorkflowFunctionMiddlewareArgs {
+  return VALID_PAYLOAD_TYPES.has(args.payload.type);
+}
+
+// TODO :: might be important for upcoming CLI compatability. Remove if not.
+// function selectToken(context: Context): string | undefined {
+//   return context.botToken !== undefined ? context.botToken : context.userToken;
+// }
+
+/**
+ * Factory for `complete()` utility
+ * @param args function_executed event
+ */
+function createFunctionComplete(
+  args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
+): FunctionCompleteFn {
+  const {
+    // context,  // TODO : remove if not helpful for CLI
+    client,
+    payload: { function_execution_id },
+  } = args;
+  // const token = selectToken(context); // TODO : remove if not helpful for CLI
+
+  return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
+    outputs: params.outputs,
+    function_execution_id,
+  });
+
+  // return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.apiCall('functions.completeSuccess', {
+  //   outputs: params.outputs,
+  //   function_execution_id,
+  // });
+}
+
+/**
+ * Factory for `fail()` utility
+ * @param args function_executed event
+ */
+function createFunctionFail(
+  args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
+): FunctionFailFn {
+  const {
+    // context,  // TODO : remove if not helpful for CLI
+    client,
+    payload: { function_execution_id },
+  } = args;
+  // const token = selectToken(context);  // TODO : remove if not helpful for CLI
+
+  return (params: Parameters<FunctionFailFn>[0]) => {
+    const { error } = params ?? {};
+
+    return client.functions.completeError({
+      error,
+      function_execution_id,
+    });
+
+    // return client.apiCall('functions.completeError', {
+    //   function_execution_id,
+    //   error,
+    // });
+  };
+}
+
+/**
+ * `prepareFunctionArgs()` takes in a function'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
+ * */
+export function prepareFunctionArgs(args: any): AllWorkflowFunctionMiddlewareArgs {
+  const { next: _next, ...functionArgs } = args;
+  const preparedArgs: any = { ...functionArgs };
+
+  // Utility args
+  preparedArgs.function = preparedArgs.event.function; // ie, function definition
+  preparedArgs.inputs = preparedArgs.event.inputs;
+  preparedArgs.complete = createFunctionComplete(preparedArgs);
+  preparedArgs.fail = createFunctionFail(preparedArgs);
+
+  return preparedArgs;
+}
diff --git a/src/errors.ts b/src/errors.ts
index b8ed369c7..3f7db700b 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -34,6 +34,8 @@ export enum ErrorCode {
   UnknownError = 'slack_bolt_unknown_error',
 
   WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error',
+
+  WorkflowFunctionInitializationError = 'slack_bolt_workflow_function_initialization_error',
 }
 
 export class UnknownError extends Error implements CodedError {
@@ -138,3 +140,7 @@ export class MultipleListenerError extends Error implements CodedError {
 export class WorkflowStepInitializationError extends Error implements CodedError {
   public code = ErrorCode.WorkflowStepInitializationError;
 }
+
+export class WorkflowFunctionInitializationError extends Error implements CodedError {
+  public code = ErrorCode.WorkflowFunctionInitializationError;
+}
diff --git a/src/types/events/base-events.ts b/src/types/events/base-events.ts
index 8c5bdb9be..cb9cd793d 100644
--- a/src/types/events/base-events.ts
+++ b/src/types/events/base-events.ts
@@ -35,6 +35,7 @@ export type SlackEvent =
   | FilePublicEvent
   | FileSharedEvent
   | FileUnsharedEvent
+  | FunctionExecutedEvent
   | GridMigrationFinishedEvent
   | GridMigrationStartedEvent
   | GroupArchiveEvent
@@ -425,6 +426,40 @@ export interface FileUnsharedEvent {
   event_ts: string;
 }
 
+export interface FunctionParams {
+  type?: string,
+  name?: string,
+  description?: string,
+  title?: string,
+  is_required?: boolean,
+}
+
+export interface FunctionInputValues {
+  [key: string]: unknown;
+}
+
+export type FunctionOutputValues = FunctionInputValues;
+
+// TODO :: Update this with new payload info
+export interface FunctionExecutedEvent {
+  type: 'function_executed',
+  function: {
+    id: string,
+    callback_id: string,
+    title: string,
+    description: string,
+    type: string,
+    input_parameters: FunctionParams[],
+    output_parameters: FunctionParams[],
+    app_id: string,
+    date_updated: number,
+  },
+  function_execution_id: string,
+  inputs: FunctionInputValues,
+  workflow_token: string, // xwfp-xxxxxxxxxxx
+  event_ts: string,
+}
+
 export interface GridMigrationFinishedEvent {
   type: 'grid_migration_finished';
   enterprise_id: string;

From 188813cc4990daeb22341025dacf496b596dfe8b Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Thu, 9 Nov 2023 17:20:14 -0800
Subject: [PATCH 02/13] [WIP] Add foundation of CLI hooks support

---
 bin/cli/check-update.js | 295 ++++++++++++++++++++++++++++++++++++++++
 bin/cli/get-hooks.js    |  17 +++
 bin/cli/get-manifest.js |  80 +++++++++++
 bin/cli/start.js        |  43 ++++++
 package.json            |   6 +
 src/WorkflowFunction.ts |   4 +-
 6 files changed, 443 insertions(+), 2 deletions(-)
 create mode 100755 bin/cli/check-update.js
 create mode 100755 bin/cli/get-hooks.js
 create mode 100755 bin/cli/get-manifest.js
 create mode 100755 bin/cli/start.js

diff --git a/bin/cli/check-update.js b/bin/cli/check-update.js
new file mode 100755
index 000000000..7f9d21246
--- /dev/null
+++ b/bin/cli/check-update.js
@@ -0,0 +1,295 @@
+#!/usr/bin/env node --no-warnings
+const fs = require('fs');
+const path = require('path');
+const util = require('util');
+const exec = util.promisify(require('child_process').exec);
+
+const SLACK_BOLT_SDK = '@slack/bolt';
+let checkUpdateExports = {};
+
+/** 
+ * Implements the check-update hook and looks for available SDK updates.
+ * Returns an object detailing info on Slack dependencies to pass up to the CLI.
+*/
+(async function _(cwd) {
+  let updates = await checkForSDKUpdates(cwd);
+  console.log(JSON.stringify(updates)); // stdout
+}(process.cwd()));
+
+/**
+ * Wraps and parses the output of the exec() function
+ * @param command the command being run by the exec() function
+ */
+async function execWrapper (command) {
+  const { stdout } = await exec(command);
+  return stdout.trim();
+}
+
+/**
+ * Checks for available SDK updates for specified dependencies, creates a version map,
+ * and then wraps everything up into a response to be passed to the CLI.
+ * @param cwd the current working directory
+ */
+async function checkForSDKUpdates(cwd) {
+  const { versionMap, inaccessibleFiles } = await createVersionMap(cwd);
+  const updateResp = createUpdateResp(versionMap, inaccessibleFiles);
+  return updateResp;
+}
+
+/**
+ * Create a version map that contains each (Slack) dependency, detailing info about
+ * current and latest versions, as well as if breaking changes are present or if there
+ * were any errors with getting version retrieval.
+ * @param cwd the current working directory of the CLI project
+ */
+async function createVersionMap(cwd) {
+  const { versionMap, inaccessibleFiles } = await readProjectDependencies(cwd);
+
+  if (versionMap && versionMap[SLACK_BOLT_SDK]) {
+    const current = versionMap[SLACK_BOLT_SDK].current || '';
+    let latest = '',
+      error = null;
+
+    try {
+      latest = await fetchLatestModuleVersion(SLACK_BOLT_SDK);
+    } catch (err) {
+      error = err;
+    }
+    const update = !!current && !!latest && current !== latest;
+    const breaking = hasBreakingChange(current, latest);
+
+    versionMap[SLACK_BOLT_SDK] = {
+      ...versionMap[SLACK_BOLT_SDK],
+      latest,
+      update,
+      breaking,
+      error,
+    };
+  }
+
+  return { versionMap, inaccessibleFiles };
+}
+
+/**
+ * Reads project dependencies - cycles through project dependency files, extracts
+ * the listed dependencies, and adds it in to the version map with version info.
+ * @param cwd the current working directory of the CLI project
+ */
+async function readProjectDependencies(cwd) {
+  const versionMap = {};
+  const { dependencyFile, inaccessibleFiles } = await gatherDependencyFiles(
+    cwd
+  );
+
+  try {
+    const [sdk, value] =
+      await checkUpdateExports.extractDependencies(dependencyFile.body, dependencyFile.name);
+
+    if (sdk !== '' && sdk === SLACK_BOLT_SDK) {
+      versionMap[SLACK_BOLT_SDK] = {
+        name: sdk,
+        current: value.version,
+      };
+    }
+  } catch (err) {
+    inaccessibleFiles.push({ name: dependencyFile.name, error: err });
+  }
+
+  return { versionMap, inaccessibleFiles };
+}
+
+/**
+ * Reads and parses JSON file - if it works, returns the file contents.
+ * If not, returns an empty object
+ * @param filePath the path of the file being read
+ */
+async function getJSON(filePath) {
+  let fileContents = {};
+  try {
+    if (fs.existsSync(filePath)) {
+      fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'));
+    } else {
+      throw new Error('Cannot find a file at path ' + filePath);
+    }
+  } catch (err) {
+    throw new Error(err);
+  }
+  return fileContents;
+}
+
+/**
+ * Gathers all related dependency files for the CLI project (package.json).
+ * @param cwd the current working directory of the CLI project
+ */
+async function gatherDependencyFiles(cwd) {
+  const { jsonDepFile, inaccessibleFiles } = await getJSONFiles(cwd);
+  const dependencyFile = jsonDepFile;
+  return { dependencyFile, inaccessibleFiles };
+}
+
+/**
+ * Gets the needed files that contain dependency info (package.json).
+ * @param cwd the current working directory of the CLI project
+ */
+async function getJSONFiles(cwd) {
+  const packageJSON = 'package.json';
+  const jsonDepFile = {};
+  const inaccessibleFiles = [];
+
+  try {
+    const jsonFile = await getJSON(`${cwd}/${packageJSON}`);
+    const jsonIsParsable =
+      jsonFile &&
+      typeof jsonFile === 'object' &&
+      !Array.isArray(jsonFile) &&
+      jsonFile.dependencies;
+
+    if (jsonIsParsable) {
+      jsonDepFile.name = packageJSON;
+      jsonDepFile.body= jsonFile;
+    }
+  } catch (err) {
+    inaccessibleFiles.push({ name: packageJSON, error: err });
+  }
+
+  return { jsonDepFile, inaccessibleFiles };
+}
+
+/**
+ * Pulls dependencies from a given file and JSON structure.
+ * @param json JSON information that includes dependencies
+ * @param fileName name of the file that the dependency list is coming from
+ */
+checkUpdateExports.extractDependencies = async (json, fileName) => {
+  // Determine if the JSON passed is an object
+  const jsonIsParsable =
+    json !== null && typeof json === 'object' && !Array.isArray(json);
+
+  if (jsonIsParsable) {
+    let boltCurrentVersion = '';
+    if (json['dependencies']['@slack/bolt']) {
+      const boltCurrentVersionOutput = JSON.parse(
+        await checkUpdateExports.getBoltCurrentVersion()
+      );
+
+      if (boltCurrentVersionOutput !== '') {
+        boltCurrentVersion =
+          boltCurrentVersionOutput['dependencies']['@slack/bolt']['version'];
+      }
+    }
+
+    return [
+      '@slack/bolt',
+      {
+        version: boltCurrentVersion,
+      },
+    ];
+  }
+
+  return [];
+};
+
+/**
+ * Gets the latest module version.
+ * @param moduleName the module that the latest version is being queried for
+ */
+async function fetchLatestModuleVersion(moduleName) {
+  let command = '';
+  if (moduleName === '@slack/bolt') {
+    command = `npm info ${moduleName} version --tag next-gen`;
+  } else if (moduleName === '@slack/deno-slack-sdk') {
+    command = `npm info ${moduleName} version --tag latest`;
+  }
+  const stdout = await execWrapper(command);
+
+  return stdout;
+}
+
+/**
+ * Checks if a dependency's upgrade from a current to the latest version will cause a
+ * breaking change.
+ * @param current current dependency version in project
+ * @param latest most up-to-date dependency version available on NPM
+ */
+function hasBreakingChange(current, latest) {
+  return current !== latest;
+}
+
+/**
+ * Creates the update response - returns an object in the expected response format.
+ * @param versionMap version map of checked dependencies, current versions, and info on upgrade + breaking changes
+ * @param inaccessibleFiles array of files that could not be read or accessed
+ */
+function createUpdateResp(versionMap, inaccessibleFiles) {
+  const name = 'the Slack SDK';
+  const releases = [];
+  const url = 'https://api.slack.com/future/changelog';
+  const fileErrorMsg = createFileErrorMsg(inaccessibleFiles);
+
+  let error = null;
+  let errorMsg = '';
+  let message = '';
+
+
+  // Output information for each dependency
+  for (const sdk of Object.values(versionMap)) {
+    // Dependency has an update OR the fetch of update failed
+    if (sdk) {
+      releases.push(sdk);
+
+      // Add the dependency that failed to be fetched to the top-level error message
+      if (sdk.error && sdk.error.message) {
+        errorMsg += errorMsg
+          ? `, ${sdk}`
+          : `An error occurred fetching updates for the following packages: ${sdk.name}`;
+      }
+    }
+  }
+
+  // Surface release notes for breaking changes
+  if (releases && releases[0] && releases[0].breaking) {
+    message = `Learn more about the breaking change at https://github.com/slackapi/bolt-js/releases/tag/@slack/bolt@${releases[0].latest}`
+  }
+
+  // If there were issues accessing dependency files, append error message(s)
+  if (inaccessibleFiles.length) {
+    errorMsg += errorMsg ? `\n\n   ${fileErrorMsg}` : fileErrorMsg;
+  }
+
+  if (errorMsg) {
+    error = { message: errorMsg };
+  }
+
+  return {
+    name,
+    message,
+    releases,
+    url,
+    error,
+  };
+}
+
+/**
+ * Returns error when dependency files cannot be read.
+ * @param inaccessibleFiles array of files that could not be read or accessed
+ */
+function createFileErrorMsg(inaccessibleFiles) {
+  let fileErrorMsg = '';
+
+  // There were issues with reading some of the files that were found
+  for (const file of inaccessibleFiles) {
+    fileErrorMsg += fileErrorMsg
+      ? `\n   ${file.name}: ${file.error.message}`
+      : `An error occurred while reading the following files: \n\n   ${file.name}: ${file.error.message}`;
+  }
+
+  return fileErrorMsg;
+}
+
+/**
+ * Queries for current Bolt version of the project.
+ */
+checkUpdateExports.getBoltCurrentVersion = async () => {
+  const stdout = await execWrapper('npm list @slack/bolt --depth=0 --json');
+  return stdout;
+};
\ No newline at end of file
diff --git a/bin/cli/get-hooks.js b/bin/cli/get-hooks.js
new file mode 100755
index 000000000..d8668e18d
--- /dev/null
+++ b/bin/cli/get-hooks.js
@@ -0,0 +1,17 @@
+#!/usr/bin/env node
+console.log(JSON.stringify({
+  hooks: {
+    'get-manifest': 'npx -q --no-install -p @slack/bolt slack-cli-get-manifest',
+    'check-update': 'npx -q --no-install -p @slack/bolt slack-cli-check-update',
+    start: 'npx -q --no-install -p @slack/bolt slack-cli-start',
+  },
+  config: {
+    watch: {
+      'filter-regex': '^manifest\\.(ts|js|json)$',
+      paths: [
+        '.',
+      ],
+    },
+    'sdk-managed-connection-enabled': true,
+  },
+}));
\ No newline at end of file
diff --git a/bin/cli/get-manifest.js b/bin/cli/get-manifest.js
new file mode 100755
index 000000000..303ba8afc
--- /dev/null
+++ b/bin/cli/get-manifest.js
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+const fs = require('fs');
+const path = require('path');
+
+/** 
+ * Implements the get-manifest script hook required by the Slack CLI
+ * Returns a manifest JSON string to stdout.
+*/
+(function _(cwd) {
+  let manifest = getManifestData(cwd);
+  console.log(JSON.stringify(manifest));
+}(process.cwd()));
+
+/** 
+ * Returns manifest data
+ * @param searchDir path to begin searching at
+ */
+function getManifestData(searchDir) {
+  const manifestJSON = readManifestJSONFile(searchDir, 'manifest.json');
+
+  if (!manifestJSON) {
+    const msg = 'Unable to find a manifest file in this project';
+    throw new Error(msg);
+  }
+
+  return manifestJSON;
+} 
+
+/** 
+ * Returns a manifest.json if it exists, null otherwise
+ * @param searchDir typically current working directory
+ * @param filename file to search for
+ */
+function readManifestJSONFile(searchDir, filename) {
+  let jsonFilePath, manifestJSON;
+  
+  try {
+    jsonFilePath = find(searchDir, filename);
+    if (fs.existsSync(jsonFilePath)) {
+      manifestJSON = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
+    }
+  } catch (error) {
+    console.error(error)
+    return null;
+  }
+  
+  return manifestJSON;
+}
+
+/** 
+ * Search for provided file path. 
+ * Returns full path when filename is found or null if no file found. 
+ * @param currentPath string of current path
+ * @param targetFilename filename to match
+ * @returns full file path string relative to starting path or null
+ * */
+function find(currentPath, targetFilename) {
+  //  TODO Cache searched paths and check that they haven't been explored already
+  //  This guards against rare edge case of a subdir in the file tree which is 
+  //  symlinked back to root or in such a way that creates a cycle. Can also implement
+  //  max depth check. 
+  if (currentPath.endsWith(`/${targetFilename}`)) {
+    return currentPath;
+  }
+
+  if (fs.existsSync(currentPath) && fs.lstatSync(currentPath).isDirectory()) {
+    let foundEntry;
+    const dirents = fs.readdirSync(currentPath);
+    for (const entry of dirents) {
+      if (entry !== 'node_modules') {
+        let newPath = path.resolve(currentPath, entry);
+        foundEntry = find(newPath, targetFilename);
+        if (foundEntry) {
+          return foundEntry;
+        }
+      }
+    }
+    return null;
+  }
+}
\ No newline at end of file
diff --git a/bin/cli/start.js b/bin/cli/start.js
new file mode 100755
index 000000000..c3d1bfbe5
--- /dev/null
+++ b/bin/cli/start.js
@@ -0,0 +1,43 @@
+#!/usr/bin/env node
+const { spawn } = require('child_process');
+const path = require('path');
+const fs = require('fs');
+const { main: pkgJSONMain } = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
+
+// Run script hook verifies that requirements for running an App in
+// in developerMode (via Socket Mode) are met
+(function _(cwd, customPath) {
+  // TODO - Format so that its less miss-able in output
+  console.log('Preparing local run in developer mode (Socket Mode)');
+  // Check required local run tokens
+  validate();
+
+  // tries the provided path, then package.json main, then defaults to index.js in the current 
+  // working directory
+  const pkgJSONDefault = 'index.js';
+  const fullPath = path.resolve(cwd, customPath ? customPath : pkgJSONMain ? pkgJSONMain : pkgJSONDefault);
+  console.log(fullPath);
+
+  // Kick off a subprocess to run the app in development mode
+  const app = spawn('node', [`${fullPath}`]);
+  app.stdout.setEncoding('utf-8');
+  app.stdout.on('data', (data) => {
+    process.stdout.write(data);
+  });
+  app.stderr.on('data', (data) => {
+    process.stderr.write(data);
+  });
+
+  app.on('close', (code) => {
+    console.log(`bolt-app local run exited with code ${code}`);
+  });
+}(process.cwd(), process.env.SLACK_CLI_CUSTOM_FILE_PATH));
+
+function validate() {
+  if (!process.env.SLACK_CLI_XOXB) {
+    throw new Error('Missing local run bot token. Please see slack-cli maintainers to troubleshoot.');
+  }
+  if (!process.env.SLACK_CLI_XAPP) {
+    throw new Error('Missing local run app token. Please see slack-cli maintainers to troubleshoot');
+  }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 9312fd59c..55d13921c 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,12 @@
     "test:types": "tsd",
     "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
   },
+  "bin": {
+    "slack-cli-get-hooks": "./bin/cli/get-hooks.js",
+    "slack-cli-get-manifest": "./bin/cli/get-manifest.js",
+    "slack-cli-check-update": "./bin/cli/check-update.js",
+    "slack-cli-start": "./bin/cli/start.js"
+  },
   "repository": "slackapi/bolt",
   "homepage": "https://slack.dev/bolt-js",
   "bugs": {
diff --git a/src/WorkflowFunction.ts b/src/WorkflowFunction.ts
index 23609f0d6..ac3dbb28a 100644
--- a/src/WorkflowFunction.ts
+++ b/src/WorkflowFunction.ts
@@ -38,7 +38,7 @@ export interface FunctionFailFn {
 }
 
 export interface WorkflowFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
-  function: FunctionExecutedEvent;
+  definition: FunctionExecutedEvent;
   complete: FunctionCompleteFn;
   fail: FunctionFailFn;
 }
@@ -208,7 +208,7 @@ export function prepareFunctionArgs(args: any): AllWorkflowFunctionMiddlewareArg
   const preparedArgs: any = { ...functionArgs };
 
   // Utility args
-  preparedArgs.function = preparedArgs.event.function; // ie, function definition
+  preparedArgs.definition = preparedArgs.event.function; // ie, function definition
   preparedArgs.inputs = preparedArgs.event.inputs;
   preparedArgs.complete = createFunctionComplete(preparedArgs);
   preparedArgs.fail = createFunctionFail(preparedArgs);

From f31d8df3d8b815412a206cdc84991649d6d837d0 Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Fri, 8 Dec 2023 14:24:13 -0800
Subject: [PATCH 03/13] [WIP] Add support for interactivity

---
 src/App.ts              | 47 ++++++++++++++++++++++++++-
 src/WorkflowFunction.ts | 71 +++++++++++++++++++++--------------------
 2 files changed, 82 insertions(+), 36 deletions(-)

diff --git a/src/App.ts b/src/App.ts
index 906b6d66a..dc19f7353 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -61,7 +61,7 @@ import { AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
 import { StringIndexed } from './types/helpers';
 // eslint-disable-next-line import/order
 import allSettled = require('promise.allsettled'); // eslint-disable-line @typescript-eslint/no-require-imports
-import { WorkflowFunction, WorkflowFunctionMiddleware } from './WorkflowFunction';
+import { FunctionCompleteFn, FunctionFailFn, WorkflowFunction, WorkflowFunctionMiddleware } from './WorkflowFunction';
 // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs
 const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
 
@@ -949,12 +949,17 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       }
     }
 
+    const { functionExecutionId, functionBotToken } = extractFunctionContext(body);
+
     const context: Context = {
       ...authorizeResult,
       ...event.customProperties,
       isEnterpriseInstall,
       retryNum: event.retryNum,
       retryReason: event.retryReason,
+      // Only add function-related props to context if truthy
+      ...(functionExecutionId && { functionExecutionId }),
+      ...(functionBotToken && { functionBotToken }),
     };
 
     // Factory for say() utility
@@ -1012,6 +1017,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       /** Ack function might be set below */
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       ack?: AckFn<any>;
+      complete?: FunctionCompleteFn;
+      fail?: FunctionFailFn;
     } = {
       body: bodyArg,
       payload,
@@ -1067,6 +1074,25 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     let { client } = this;
     const token = selectToken(context);
 
+    // TODO :: WorkflowFunction owns these same utilities. Rework TS to allow for reuse.
+    // Set complete() and fail() utilities for function-related interactivity
+    if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
+      listenerArgs.complete = (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
+        token: context.functionBotToken,
+        outputs: params.outputs || {},
+        function_execution_id: context.functionExecutionId,
+      });
+      listenerArgs.fail = (params: Parameters<FunctionFailFn>[0]) => {
+        const { error } = params ?? {};
+
+        return client.functions.completeError({
+          token: context.functionBotToken,
+          error,
+          function_execution_id: context.functionExecutionId,
+        });
+      };
+    }
+
     if (token !== undefined) {
       let pool;
       const clientOptionsCopy = { ...this.clientOptions };
@@ -1573,6 +1599,25 @@ function escapeHtml(input: string | undefined | null): string {
   return '';
 }
 
+function extractFunctionContext(body: StringIndexed) {
+  let functionExecutionId;
+  let functionBotToken;
+
+  // function_executed event
+  if (body.event && body.event.function_execution_id) {
+    functionExecutionId = body.event.function_execution_id;
+    functionBotToken = body.event.bot_access_token;
+  }
+
+  // interactivity (block_actions)
+  if (body.function_data) {
+    functionExecutionId = body.function_data.execution_id;
+    functionBotToken = body.bot_access_token;
+  }
+
+  return { functionExecutionId, functionBotToken };
+}
+
 // ----------------------------
 // Instrumentation
 // Don't change the position of the following code
diff --git a/src/WorkflowFunction.ts b/src/WorkflowFunction.ts
index ac3dbb28a..ae5ed65c8 100644
--- a/src/WorkflowFunction.ts
+++ b/src/WorkflowFunction.ts
@@ -1,16 +1,15 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 import {
-  WorkflowsStepCompletedResponse,
-  WorkflowsStepFailedResponse,
-  // FunctionCompleteErrorResponse,
-  // FunctionCompleteSuccessResponse,
+  WebClient,
+  FunctionsCompleteErrorResponse,
+  FunctionsCompleteSuccessResponse,
 } from '@slack/web-api';
 import {
   Middleware,
   AllMiddlewareArgs,
   AnyMiddlewareArgs,
   SlackEventMiddlewareArgs,
-  FunctionExecutedEvent,
+  Context,
 } from './types';
 import processMiddleware from './middleware/process';
 import { WorkflowFunctionInitializationError } from './errors';
@@ -28,17 +27,14 @@ export interface FunctionFailArguments {
 }
 
 export interface FunctionCompleteFn {
-  // TODO :: import FunctionCompleteErrorResponse from @slack/web-api
-  (params?: FunctionCompleteArguments): Promise<WorkflowsStepCompletedResponse>;
+  (params?: FunctionCompleteArguments): Promise<FunctionsCompleteSuccessResponse>;
 }
 
 export interface FunctionFailFn {
-  // TODO :: import FunctionCompleteErrorResponse from @slack/web-api
-  (params: FunctionFailArguments): Promise<WorkflowsStepFailedResponse>;
+  (params: FunctionFailArguments): Promise<FunctionsCompleteErrorResponse>;
 }
 
 export interface WorkflowFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
-  definition: FunctionExecutedEvent;
   complete: FunctionCompleteFn;
   fail: FunctionFailFn;
 }
@@ -64,7 +60,6 @@ export class WorkflowFunction {
   /** Function callback_id */
   public callbackId: string;
 
-  /** Function definition */
   private middleware: WorkflowFunctionMiddleware;
 
   public constructor(
@@ -110,8 +105,13 @@ export function validate(callbackId: string, listeners: WorkflowFunctionMiddlewa
     throw new WorkflowFunctionInitializationError(errorMsg);
   }
 
-  // TODO: Validate that all array members are functions
-  console.log(listeners);
+  // Ensure all listeners are functions
+  listeners.forEach((listener) => {
+    if (!(listener instanceof Function)) {
+      const errorMsg = 'All WorkflowFunction listeners must be functions';
+      throw new WorkflowFunctionInitializationError(errorMsg);
+    }
+  });
 }
 
 /**
@@ -138,62 +138,59 @@ export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllWorkflowFun
   return VALID_PAYLOAD_TYPES.has(args.payload.type);
 }
 
-// TODO :: might be important for upcoming CLI compatability. Remove if not.
-// function selectToken(context: Context): string | undefined {
-//   return context.botToken !== undefined ? context.botToken : context.userToken;
-// }
+function selectToken(context: Context): string | undefined {
+  return context.functionBotToken ? context.functionBotToken : context.botToken || context.userToken;
+}
 
 /**
  * Factory for `complete()` utility
  * @param args function_executed event
  */
-function createFunctionComplete(
+export function createFunctionComplete(
   args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
 ): FunctionCompleteFn {
   const {
-    // context,  // TODO : remove if not helpful for CLI
+    context,
     client,
     payload: { function_execution_id },
   } = args;
-  // const token = selectToken(context); // TODO : remove if not helpful for CLI
+  const token = selectToken(context);
 
   return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
-    outputs: params.outputs,
+    // TODO :: Possible to change to context.functionBotToken, but need
+    // to establish if there will ever be a case where botToken would be used instead
+    // as a fallback
+    token,
+    outputs: params.outputs || {},
     function_execution_id,
   });
-
-  // return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.apiCall('functions.completeSuccess', {
-  //   outputs: params.outputs,
-  //   function_execution_id,
-  // });
 }
 
 /**
  * Factory for `fail()` utility
  * @param args function_executed event
  */
-function createFunctionFail(
+export function createFunctionFail(
   args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
 ): FunctionFailFn {
   const {
-    // context,  // TODO : remove if not helpful for CLI
+    context,
     client,
     payload: { function_execution_id },
   } = args;
-  // const token = selectToken(context);  // TODO : remove if not helpful for CLI
+  const token = selectToken(context);
 
   return (params: Parameters<FunctionFailFn>[0]) => {
     const { error } = params ?? {};
 
     return client.functions.completeError({
+      // TODO :: Possible to change to context.functionBotToken, but need
+      // to establish if there will ever be a case where botToken would be used instead
+      // as a fallback
+      token,
       error,
       function_execution_id,
     });
-
-    // return client.apiCall('functions.completeError', {
-    //   function_execution_id,
-    //   error,
-    // });
   };
 }
 
@@ -207,8 +204,12 @@ export function prepareFunctionArgs(args: any): AllWorkflowFunctionMiddlewareArg
   const { next: _next, ...functionArgs } = args;
   const preparedArgs: any = { ...functionArgs };
 
+  // The use of a functionBotToken establishes continuity between
+  // a function_executed event and subsequent interactive events
+  const client = new WebClient(preparedArgs.context.functionBotToken, { ...functionArgs.client });
+  preparedArgs.client = client;
+
   // Utility args
-  preparedArgs.definition = preparedArgs.event.function; // ie, function definition
   preparedArgs.inputs = preparedArgs.event.inputs;
   preparedArgs.complete = createFunctionComplete(preparedArgs);
   preparedArgs.fail = createFunctionFail(preparedArgs);

From 3faab87c7009eae3edd8db773ebbc0cf5c6353da Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Thu, 14 Dec 2023 16:24:55 -0800
Subject: [PATCH 04/13] [WIP] Add attachFunctionToken opt-out option

---
 src/App.ts              | 20 ++++++++++++++------
 src/WorkflowFunction.ts | 14 +++++---------
 2 files changed, 19 insertions(+), 15 deletions(-)

diff --git a/src/App.ts b/src/App.ts
index dc19f7353..6a98ef6ac 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -109,6 +109,7 @@ export interface AppOptions {
   tokenVerificationEnabled?: boolean;
   deferInitialization?: boolean;
   extendedErrorHandler?: boolean;
+  attachFunctionToken?: boolean;
 }
 
 export { LogLevel, Logger } from '@slack/logger';
@@ -269,6 +270,8 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
 
   private initialized: boolean;
 
+  private attachFunctionToken: boolean;
+
   public constructor({
     signingSecret = undefined,
     endpoints = undefined,
@@ -301,6 +304,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     tokenVerificationEnabled = true,
     extendedErrorHandler = false,
     deferInitialization = false,
+    attachFunctionToken = true,
   }: AppOptions = {}) {
     /* ------------------------ Developer mode ----------------------------- */
     this.developerMode = developerMode;
@@ -333,6 +337,9 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     this.errorHandler = defaultErrorHandler(this.logger) as AnyErrorHandler;
     this.extendedErrorHandler = extendedErrorHandler;
 
+    // Override token with functionBotToken in function-related handlers
+    this.attachFunctionToken = attachFunctionToken;
+
     /* ------------------------ Set client options ------------------------*/
     this.clientOptions = clientOptions !== undefined ? clientOptions : {};
     if (agent !== undefined && this.clientOptions.agent === undefined) {
@@ -949,19 +956,20 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       }
     }
 
-    const { functionExecutionId, functionBotToken } = extractFunctionContext(body);
-
     const context: Context = {
       ...authorizeResult,
       ...event.customProperties,
       isEnterpriseInstall,
       retryNum: event.retryNum,
       retryReason: event.retryReason,
-      // Only add function-related props to context if truthy
-      ...(functionExecutionId && { functionExecutionId }),
-      ...(functionBotToken && { functionBotToken }),
     };
 
+    if (this.attachFunctionToken) {
+      const { functionExecutionId, functionBotToken } = extractFunctionContext(body);
+      if (functionExecutionId) { context.functionExecutionId = functionExecutionId; }
+      if (functionBotToken) { context.functionBotToken = functionBotToken; }
+    }
+
     // Factory for say() utility
     const createSay = (channelId: string): SayFn => {
       const token = selectToken(context);
@@ -1604,7 +1612,7 @@ function extractFunctionContext(body: StringIndexed) {
   let functionBotToken;
 
   // function_executed event
-  if (body.event && body.event.function_execution_id) {
+  if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
     functionExecutionId = body.event.function_execution_id;
     functionBotToken = body.event.bot_access_token;
   }
diff --git a/src/WorkflowFunction.ts b/src/WorkflowFunction.ts
index ae5ed65c8..848cd7a48 100644
--- a/src/WorkflowFunction.ts
+++ b/src/WorkflowFunction.ts
@@ -139,6 +139,7 @@ export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllWorkflowFun
 }
 
 function selectToken(context: Context): string | undefined {
+  // If attachFunctionToken = false, fallback to botToken or userToken
   return context.functionBotToken ? context.functionBotToken : context.botToken || context.userToken;
 }
 
@@ -157,9 +158,6 @@ export function createFunctionComplete(
   const token = selectToken(context);
 
   return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
-    // TODO :: Possible to change to context.functionBotToken, but need
-    // to establish if there will ever be a case where botToken would be used instead
-    // as a fallback
     token,
     outputs: params.outputs || {},
     function_execution_id,
@@ -184,9 +182,6 @@ export function createFunctionFail(
     const { error } = params ?? {};
 
     return client.functions.completeError({
-      // TODO :: Possible to change to context.functionBotToken, but need
-      // to establish if there will ever be a case where botToken would be used instead
-      // as a fallback
       token,
       error,
       function_execution_id,
@@ -203,10 +198,11 @@ export function createFunctionFail(
 export function prepareFunctionArgs(args: any): AllWorkflowFunctionMiddlewareArgs {
   const { next: _next, ...functionArgs } = args;
   const preparedArgs: any = { ...functionArgs };
+  const token = selectToken(functionArgs.context);
 
-  // The use of a functionBotToken establishes continuity between
-  // a function_executed event and subsequent interactive events
-  const client = new WebClient(preparedArgs.context.functionBotToken, { ...functionArgs.client });
+  // Making calls with a functionBotToken establishes continuity between
+  // a function_executed event and subsequent interactive events (actions)
+  const client = new WebClient(token, { ...functionArgs.client });
   preparedArgs.client = client;
 
   // Utility args

From b46f47029aa4f9e64eb057f0e3b73a9d57d581b4 Mon Sep 17 00:00:00 2001
From: Ethan Zimbelman <ethan.zimbelman@me.com>
Date: Fri, 5 Jan 2024 11:07:52 -0800
Subject: [PATCH 05/13] [WIP] Revert CLI hook support within Bolt

---
 bin/cli/check-update.js | 295 ----------------------------------------
 bin/cli/get-hooks.js    |  17 ---
 bin/cli/get-manifest.js |  80 -----------
 bin/cli/start.js        |  43 ------
 package.json            |   6 -
 5 files changed, 441 deletions(-)
 delete mode 100755 bin/cli/check-update.js
 delete mode 100755 bin/cli/get-hooks.js
 delete mode 100755 bin/cli/get-manifest.js
 delete mode 100755 bin/cli/start.js

diff --git a/bin/cli/check-update.js b/bin/cli/check-update.js
deleted file mode 100755
index 7f9d21246..000000000
--- a/bin/cli/check-update.js
+++ /dev/null
@@ -1,295 +0,0 @@
-#!/usr/bin/env node --no-warnings
-const fs = require('fs');
-const path = require('path');
-const util = require('util');
-const exec = util.promisify(require('child_process').exec);
-
-const SLACK_BOLT_SDK = '@slack/bolt';
-let checkUpdateExports = {};
-
-/** 
- * Implements the check-update hook and looks for available SDK updates.
- * Returns an object detailing info on Slack dependencies to pass up to the CLI.
-*/
-(async function _(cwd) {
-  let updates = await checkForSDKUpdates(cwd);
-  console.log(JSON.stringify(updates)); // stdout
-}(process.cwd()));
-
-/**
- * Wraps and parses the output of the exec() function
- * @param command the command being run by the exec() function
- */
-async function execWrapper (command) {
-  const { stdout } = await exec(command);
-  return stdout.trim();
-}
-
-/**
- * Checks for available SDK updates for specified dependencies, creates a version map,
- * and then wraps everything up into a response to be passed to the CLI.
- * @param cwd the current working directory
- */
-async function checkForSDKUpdates(cwd) {
-  const { versionMap, inaccessibleFiles } = await createVersionMap(cwd);
-  const updateResp = createUpdateResp(versionMap, inaccessibleFiles);
-  return updateResp;
-}
-
-/**
- * Create a version map that contains each (Slack) dependency, detailing info about
- * current and latest versions, as well as if breaking changes are present or if there
- * were any errors with getting version retrieval.
- * @param cwd the current working directory of the CLI project
- */
-async function createVersionMap(cwd) {
-  const { versionMap, inaccessibleFiles } = await readProjectDependencies(cwd);
-
-  if (versionMap && versionMap[SLACK_BOLT_SDK]) {
-    const current = versionMap[SLACK_BOLT_SDK].current || '';
-    let latest = '',
-      error = null;
-
-    try {
-      latest = await fetchLatestModuleVersion(SLACK_BOLT_SDK);
-    } catch (err) {
-      error = err;
-    }
-    const update = !!current && !!latest && current !== latest;
-    const breaking = hasBreakingChange(current, latest);
-
-    versionMap[SLACK_BOLT_SDK] = {
-      ...versionMap[SLACK_BOLT_SDK],
-      latest,
-      update,
-      breaking,
-      error,
-    };
-  }
-
-  return { versionMap, inaccessibleFiles };
-}
-
-/**
- * Reads project dependencies - cycles through project dependency files, extracts
- * the listed dependencies, and adds it in to the version map with version info.
- * @param cwd the current working directory of the CLI project
- */
-async function readProjectDependencies(cwd) {
-  const versionMap = {};
-  const { dependencyFile, inaccessibleFiles } = await gatherDependencyFiles(
-    cwd
-  );
-
-  try {
-    const [sdk, value] =
-      await checkUpdateExports.extractDependencies(dependencyFile.body, dependencyFile.name);
-
-    if (sdk !== '' && sdk === SLACK_BOLT_SDK) {
-      versionMap[SLACK_BOLT_SDK] = {
-        name: sdk,
-        current: value.version,
-      };
-    }
-  } catch (err) {
-    inaccessibleFiles.push({ name: dependencyFile.name, error: err });
-  }
-
-  return { versionMap, inaccessibleFiles };
-}
-
-/**
- * Reads and parses JSON file - if it works, returns the file contents.
- * If not, returns an empty object
- * @param filePath the path of the file being read
- */
-async function getJSON(filePath) {
-  let fileContents = {};
-  try {
-    if (fs.existsSync(filePath)) {
-      fileContents = JSON.parse(fs.readFileSync(filePath, 'utf8'));
-    } else {
-      throw new Error('Cannot find a file at path ' + filePath);
-    }
-  } catch (err) {
-    throw new Error(err);
-  }
-  return fileContents;
-}
-
-/**
- * Gathers all related dependency files for the CLI project (package.json).
- * @param cwd the current working directory of the CLI project
- */
-async function gatherDependencyFiles(cwd) {
-  const { jsonDepFile, inaccessibleFiles } = await getJSONFiles(cwd);
-  const dependencyFile = jsonDepFile;
-  return { dependencyFile, inaccessibleFiles };
-}
-
-/**
- * Gets the needed files that contain dependency info (package.json).
- * @param cwd the current working directory of the CLI project
- */
-async function getJSONFiles(cwd) {
-  const packageJSON = 'package.json';
-  const jsonDepFile = {};
-  const inaccessibleFiles = [];
-
-  try {
-    const jsonFile = await getJSON(`${cwd}/${packageJSON}`);
-    const jsonIsParsable =
-      jsonFile &&
-      typeof jsonFile === 'object' &&
-      !Array.isArray(jsonFile) &&
-      jsonFile.dependencies;
-
-    if (jsonIsParsable) {
-      jsonDepFile.name = packageJSON;
-      jsonDepFile.body= jsonFile;
-    }
-  } catch (err) {
-    inaccessibleFiles.push({ name: packageJSON, error: err });
-  }
-
-  return { jsonDepFile, inaccessibleFiles };
-}
-
-/**
- * Pulls dependencies from a given file and JSON structure.
- * @param json JSON information that includes dependencies
- * @param fileName name of the file that the dependency list is coming from
- */
-checkUpdateExports.extractDependencies = async (json, fileName) => {
-  // Determine if the JSON passed is an object
-  const jsonIsParsable =
-    json !== null && typeof json === 'object' && !Array.isArray(json);
-
-  if (jsonIsParsable) {
-    let boltCurrentVersion = '';
-    if (json['dependencies']['@slack/bolt']) {
-      const boltCurrentVersionOutput = JSON.parse(
-        await checkUpdateExports.getBoltCurrentVersion()
-      );
-
-      if (boltCurrentVersionOutput !== '') {
-        boltCurrentVersion =
-          boltCurrentVersionOutput['dependencies']['@slack/bolt']['version'];
-      }
-    }
-
-    return [
-      '@slack/bolt',
-      {
-        version: boltCurrentVersion,
-      },
-    ];
-  }
-
-  return [];
-};
-
-/**
- * Gets the latest module version.
- * @param moduleName the module that the latest version is being queried for
- */
-async function fetchLatestModuleVersion(moduleName) {
-  let command = '';
-  if (moduleName === '@slack/bolt') {
-    command = `npm info ${moduleName} version --tag next-gen`;
-  } else if (moduleName === '@slack/deno-slack-sdk') {
-    command = `npm info ${moduleName} version --tag latest`;
-  }
-  const stdout = await execWrapper(command);
-
-  return stdout;
-}
-
-/**
- * Checks if a dependency's upgrade from a current to the latest version will cause a
- * breaking change.
- * @param current current dependency version in project
- * @param latest most up-to-date dependency version available on NPM
- */
-function hasBreakingChange(current, latest) {
-  return current !== latest;
-}
-
-/**
- * Creates the update response - returns an object in the expected response format.
- * @param versionMap version map of checked dependencies, current versions, and info on upgrade + breaking changes
- * @param inaccessibleFiles array of files that could not be read or accessed
- */
-function createUpdateResp(versionMap, inaccessibleFiles) {
-  const name = 'the Slack SDK';
-  const releases = [];
-  const url = 'https://api.slack.com/future/changelog';
-  const fileErrorMsg = createFileErrorMsg(inaccessibleFiles);
-
-  let error = null;
-  let errorMsg = '';
-  let message = '';
-
-
-  // Output information for each dependency
-  for (const sdk of Object.values(versionMap)) {
-    // Dependency has an update OR the fetch of update failed
-    if (sdk) {
-      releases.push(sdk);
-
-      // Add the dependency that failed to be fetched to the top-level error message
-      if (sdk.error && sdk.error.message) {
-        errorMsg += errorMsg
-          ? `, ${sdk}`
-          : `An error occurred fetching updates for the following packages: ${sdk.name}`;
-      }
-    }
-  }
-
-  // Surface release notes for breaking changes
-  if (releases && releases[0] && releases[0].breaking) {
-    message = `Learn more about the breaking change at https://github.com/slackapi/bolt-js/releases/tag/@slack/bolt@${releases[0].latest}`
-  }
-
-  // If there were issues accessing dependency files, append error message(s)
-  if (inaccessibleFiles.length) {
-    errorMsg += errorMsg ? `\n\n   ${fileErrorMsg}` : fileErrorMsg;
-  }
-
-  if (errorMsg) {
-    error = { message: errorMsg };
-  }
-
-  return {
-    name,
-    message,
-    releases,
-    url,
-    error,
-  };
-}
-
-/**
- * Returns error when dependency files cannot be read.
- * @param inaccessibleFiles array of files that could not be read or accessed
- */
-function createFileErrorMsg(inaccessibleFiles) {
-  let fileErrorMsg = '';
-
-  // There were issues with reading some of the files that were found
-  for (const file of inaccessibleFiles) {
-    fileErrorMsg += fileErrorMsg
-      ? `\n   ${file.name}: ${file.error.message}`
-      : `An error occurred while reading the following files: \n\n   ${file.name}: ${file.error.message}`;
-  }
-
-  return fileErrorMsg;
-}
-
-/**
- * Queries for current Bolt version of the project.
- */
-checkUpdateExports.getBoltCurrentVersion = async () => {
-  const stdout = await execWrapper('npm list @slack/bolt --depth=0 --json');
-  return stdout;
-};
\ No newline at end of file
diff --git a/bin/cli/get-hooks.js b/bin/cli/get-hooks.js
deleted file mode 100755
index d8668e18d..000000000
--- a/bin/cli/get-hooks.js
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env node
-console.log(JSON.stringify({
-  hooks: {
-    'get-manifest': 'npx -q --no-install -p @slack/bolt slack-cli-get-manifest',
-    'check-update': 'npx -q --no-install -p @slack/bolt slack-cli-check-update',
-    start: 'npx -q --no-install -p @slack/bolt slack-cli-start',
-  },
-  config: {
-    watch: {
-      'filter-regex': '^manifest\\.(ts|js|json)$',
-      paths: [
-        '.',
-      ],
-    },
-    'sdk-managed-connection-enabled': true,
-  },
-}));
\ No newline at end of file
diff --git a/bin/cli/get-manifest.js b/bin/cli/get-manifest.js
deleted file mode 100755
index 303ba8afc..000000000
--- a/bin/cli/get-manifest.js
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env node
-const fs = require('fs');
-const path = require('path');
-
-/** 
- * Implements the get-manifest script hook required by the Slack CLI
- * Returns a manifest JSON string to stdout.
-*/
-(function _(cwd) {
-  let manifest = getManifestData(cwd);
-  console.log(JSON.stringify(manifest));
-}(process.cwd()));
-
-/** 
- * Returns manifest data
- * @param searchDir path to begin searching at
- */
-function getManifestData(searchDir) {
-  const manifestJSON = readManifestJSONFile(searchDir, 'manifest.json');
-
-  if (!manifestJSON) {
-    const msg = 'Unable to find a manifest file in this project';
-    throw new Error(msg);
-  }
-
-  return manifestJSON;
-} 
-
-/** 
- * Returns a manifest.json if it exists, null otherwise
- * @param searchDir typically current working directory
- * @param filename file to search for
- */
-function readManifestJSONFile(searchDir, filename) {
-  let jsonFilePath, manifestJSON;
-  
-  try {
-    jsonFilePath = find(searchDir, filename);
-    if (fs.existsSync(jsonFilePath)) {
-      manifestJSON = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8'));
-    }
-  } catch (error) {
-    console.error(error)
-    return null;
-  }
-  
-  return manifestJSON;
-}
-
-/** 
- * Search for provided file path. 
- * Returns full path when filename is found or null if no file found. 
- * @param currentPath string of current path
- * @param targetFilename filename to match
- * @returns full file path string relative to starting path or null
- * */
-function find(currentPath, targetFilename) {
-  //  TODO Cache searched paths and check that they haven't been explored already
-  //  This guards against rare edge case of a subdir in the file tree which is 
-  //  symlinked back to root or in such a way that creates a cycle. Can also implement
-  //  max depth check. 
-  if (currentPath.endsWith(`/${targetFilename}`)) {
-    return currentPath;
-  }
-
-  if (fs.existsSync(currentPath) && fs.lstatSync(currentPath).isDirectory()) {
-    let foundEntry;
-    const dirents = fs.readdirSync(currentPath);
-    for (const entry of dirents) {
-      if (entry !== 'node_modules') {
-        let newPath = path.resolve(currentPath, entry);
-        foundEntry = find(newPath, targetFilename);
-        if (foundEntry) {
-          return foundEntry;
-        }
-      }
-    }
-    return null;
-  }
-}
\ No newline at end of file
diff --git a/bin/cli/start.js b/bin/cli/start.js
deleted file mode 100755
index c3d1bfbe5..000000000
--- a/bin/cli/start.js
+++ /dev/null
@@ -1,43 +0,0 @@
-#!/usr/bin/env node
-const { spawn } = require('child_process');
-const path = require('path');
-const fs = require('fs');
-const { main: pkgJSONMain } = JSON.parse(fs.readFileSync('./package.json', 'utf-8'));
-
-// Run script hook verifies that requirements for running an App in
-// in developerMode (via Socket Mode) are met
-(function _(cwd, customPath) {
-  // TODO - Format so that its less miss-able in output
-  console.log('Preparing local run in developer mode (Socket Mode)');
-  // Check required local run tokens
-  validate();
-
-  // tries the provided path, then package.json main, then defaults to index.js in the current 
-  // working directory
-  const pkgJSONDefault = 'index.js';
-  const fullPath = path.resolve(cwd, customPath ? customPath : pkgJSONMain ? pkgJSONMain : pkgJSONDefault);
-  console.log(fullPath);
-
-  // Kick off a subprocess to run the app in development mode
-  const app = spawn('node', [`${fullPath}`]);
-  app.stdout.setEncoding('utf-8');
-  app.stdout.on('data', (data) => {
-    process.stdout.write(data);
-  });
-  app.stderr.on('data', (data) => {
-    process.stderr.write(data);
-  });
-
-  app.on('close', (code) => {
-    console.log(`bolt-app local run exited with code ${code}`);
-  });
-}(process.cwd(), process.env.SLACK_CLI_CUSTOM_FILE_PATH));
-
-function validate() {
-  if (!process.env.SLACK_CLI_XOXB) {
-    throw new Error('Missing local run bot token. Please see slack-cli maintainers to troubleshoot.');
-  }
-  if (!process.env.SLACK_CLI_XAPP) {
-    throw new Error('Missing local run app token. Please see slack-cli maintainers to troubleshoot');
-  }
-}
\ No newline at end of file
diff --git a/package.json b/package.json
index 55d13921c..9312fd59c 100644
--- a/package.json
+++ b/package.json
@@ -34,12 +34,6 @@
     "test:types": "tsd",
     "watch": "npx nodemon --watch 'src' --ext 'ts' --exec npm run build"
   },
-  "bin": {
-    "slack-cli-get-hooks": "./bin/cli/get-hooks.js",
-    "slack-cli-get-manifest": "./bin/cli/get-manifest.js",
-    "slack-cli-check-update": "./bin/cli/check-update.js",
-    "slack-cli-start": "./bin/cli/start.js"
-  },
   "repository": "slackapi/bolt",
   "homepage": "https://slack.dev/bolt-js",
   "bugs": {

From 1b7987369894557e04028f5f98d1aaa974a3e3cd Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Wed, 24 Jan 2024 16:18:34 -0800
Subject: [PATCH 06/13] Incorporate PR comments

---
 src/App.ts                                    |  45 +++---
 ...{WorkflowFunction.ts => CustomFunction.ts} | 142 ++++++++----------
 src/errors.ts                                 |   6 +-
 src/types/events/base-events.ts               |  48 +++---
 4 files changed, 110 insertions(+), 131 deletions(-)
 rename src/{WorkflowFunction.ts => CustomFunction.ts} (53%)

diff --git a/src/App.ts b/src/App.ts
index 6a98ef6ac..9902de838 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -61,7 +61,7 @@ import { AllMiddlewareArgs, contextBuiltinKeys } from './types/middleware';
 import { StringIndexed } from './types/helpers';
 // eslint-disable-next-line import/order
 import allSettled = require('promise.allsettled'); // eslint-disable-line @typescript-eslint/no-require-imports
-import { FunctionCompleteFn, FunctionFailFn, WorkflowFunction, WorkflowFunctionMiddleware } from './WorkflowFunction';
+import { FunctionCompleteFn, FunctionFailFn, CustomFunction, CustomFunctionMiddleware } from './CustomFunction';
 // eslint-disable-next-line @typescript-eslint/no-require-imports, import/no-commonjs
 const packageJson = require('../package.json'); // eslint-disable-line @typescript-eslint/no-var-requires
 
@@ -337,7 +337,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     this.errorHandler = defaultErrorHandler(this.logger) as AnyErrorHandler;
     this.extendedErrorHandler = extendedErrorHandler;
 
-    // Override token with functionBotToken in function-related handlers
+    // Override token with functionBotAccessToken in function-related handlers
     this.attachFunctionToken = attachFunctionToken;
 
     /* ------------------------ Set client options ------------------------*/
@@ -526,10 +526,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
   }
 
   /**
- * Register WorkflowFunction middleware
+ * Register CustomFunction middleware
  */
-  public function(callbackId: string, ...listeners: WorkflowFunctionMiddleware): this {
-    const fn = new WorkflowFunction(callbackId, listeners);
+  public function(callbackId: string, ...listeners: CustomFunctionMiddleware): this {
+    const fn = new CustomFunction(callbackId, listeners);
     const m = fn.getMiddleware();
     this.middleware.push(m);
     return this;
@@ -964,10 +964,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; }
+
     if (this.attachFunctionToken) {
-      const { functionExecutionId, functionBotToken } = extractFunctionContext(body);
-      if (functionExecutionId) { context.functionExecutionId = functionExecutionId; }
-      if (functionBotToken) { context.functionBotToken = functionBotToken; }
+      if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
     }
 
     // Factory for say() utility
@@ -1082,23 +1084,10 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
     let { client } = this;
     const token = selectToken(context);
 
-    // TODO :: WorkflowFunction owns these same utilities. Rework TS to allow for reuse.
-    // Set complete() and fail() utilities for function-related interactivity
+    // Add complete() and fail() utilities for function-related interactivity
     if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {
-      listenerArgs.complete = (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
-        token: context.functionBotToken,
-        outputs: params.outputs || {},
-        function_execution_id: context.functionExecutionId,
-      });
-      listenerArgs.fail = (params: Parameters<FunctionFailFn>[0]) => {
-        const { error } = params ?? {};
-
-        return client.functions.completeError({
-          token: context.functionBotToken,
-          error,
-          function_execution_id: context.functionExecutionId,
-        });
-      };
+      listenerArgs.complete = CustomFunction.createFunctionComplete(context, client);
+      listenerArgs.fail = CustomFunction.createFunctionFail(context, client);
     }
 
     if (token !== undefined) {
@@ -1609,21 +1598,21 @@ function escapeHtml(input: string | undefined | null): string {
 
 function extractFunctionContext(body: StringIndexed) {
   let functionExecutionId;
-  let functionBotToken;
+  let functionBotAccessToken;
 
   // function_executed event
   if (body.event && body.event.type === 'function_executed' && body.event.function_execution_id) {
     functionExecutionId = body.event.function_execution_id;
-    functionBotToken = body.event.bot_access_token;
+    functionBotAccessToken = body.event.bot_access_token;
   }
 
   // interactivity (block_actions)
   if (body.function_data) {
     functionExecutionId = body.function_data.execution_id;
-    functionBotToken = body.bot_access_token;
+    functionBotAccessToken = body.bot_access_token;
   }
 
-  return { functionExecutionId, functionBotToken };
+  return { functionExecutionId, functionBotAccessToken };
 }
 
 // ----------------------------
diff --git a/src/WorkflowFunction.ts b/src/CustomFunction.ts
similarity index 53%
rename from src/WorkflowFunction.ts
rename to src/CustomFunction.ts
index 848cd7a48..b20276535 100644
--- a/src/WorkflowFunction.ts
+++ b/src/CustomFunction.ts
@@ -12,43 +12,43 @@ import {
   Context,
 } from './types';
 import processMiddleware from './middleware/process';
-import { WorkflowFunctionInitializationError } from './errors';
+import { CustomFunctionInitializationError } from './errors';
 
 /** Interfaces */
 
-export interface FunctionCompleteArguments {
+interface FunctionCompleteArguments {
   outputs?: {
     [key: string]: any;
   };
 }
 
-export interface FunctionFailArguments {
-  error: string;
-}
-
 export interface FunctionCompleteFn {
   (params?: FunctionCompleteArguments): Promise<FunctionsCompleteSuccessResponse>;
 }
 
+interface FunctionFailArguments {
+  error: string;
+}
+
 export interface FunctionFailFn {
   (params: FunctionFailArguments): Promise<FunctionsCompleteErrorResponse>;
 }
 
-export interface WorkflowFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
+export interface CustomFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
   complete: FunctionCompleteFn;
   fail: FunctionFailFn;
 }
 
 /** Types */
 
-export type SlackWorkflowFunctionMiddlewareArgs = WorkflowFunctionExecuteMiddlewareArgs;
+export type SlackCustomFunctionMiddlewareArgs = CustomFunctionExecuteMiddlewareArgs;
 
-export type WorkflowFunctionExecuteMiddleware = Middleware<WorkflowFunctionExecuteMiddlewareArgs>;
+type CustomFunctionExecuteMiddleware = Middleware<CustomFunctionExecuteMiddlewareArgs>;
 
-export type WorkflowFunctionMiddleware = WorkflowFunctionExecuteMiddleware[];
+export type CustomFunctionMiddleware = CustomFunctionExecuteMiddleware[];
 
-export type AllWorkflowFunctionMiddlewareArgs
-  <T extends SlackWorkflowFunctionMiddlewareArgs = SlackWorkflowFunctionMiddlewareArgs> = T & AllMiddlewareArgs;
+export type AllCustomFunctionMiddlewareArgs
+  <T extends SlackCustomFunctionMiddlewareArgs = SlackCustomFunctionMiddlewareArgs> = T & AllMiddlewareArgs;
 
 /** Constants */
 
@@ -56,15 +56,15 @@ const VALID_PAYLOAD_TYPES = new Set(['function_executed']);
 
 /** Class */
 
-export class WorkflowFunction {
+export class CustomFunction {
   /** Function callback_id */
   public callbackId: string;
 
-  private middleware: WorkflowFunctionMiddleware;
+  private middleware: CustomFunctionMiddleware;
 
   public constructor(
     callbackId: string,
-    middleware: WorkflowFunctionMiddleware,
+    middleware: CustomFunctionMiddleware,
   ) {
     validate(callbackId, middleware);
 
@@ -81,35 +81,69 @@ export class WorkflowFunction {
     };
   }
 
-  private matchesConstraints(args: SlackWorkflowFunctionMiddlewareArgs): boolean {
+  private matchesConstraints(args: SlackCustomFunctionMiddlewareArgs): boolean {
     return args.payload.function.callback_id === this.callbackId;
   }
 
-  private async processEvent(args: AllWorkflowFunctionMiddlewareArgs): Promise<void> {
+  private async processEvent(args: AllCustomFunctionMiddlewareArgs): Promise<void> {
     const functionArgs = prepareFunctionArgs(args);
     const stepMiddleware = this.getStepMiddleware();
     return processStepMiddleware(functionArgs, stepMiddleware);
   }
 
-  private getStepMiddleware(): WorkflowFunctionMiddleware {
+  private getStepMiddleware(): CustomFunctionMiddleware {
     return this.middleware;
   }
+
+  /**
+   * Factory for `complete()` utility
+   * @param args function_executed event
+   */
+  public static createFunctionComplete(context: Context, client: WebClient): FunctionCompleteFn {
+    const token = selectToken(context);
+    const { functionExecutionId } = context;
+
+    return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
+      token,
+      outputs: params.outputs || {},
+      function_execution_id: functionExecutionId,
+    });
+  }
+
+  /**
+ * Factory for `fail()` utility
+ * @param args function_executed event
+ */
+  public static createFunctionFail(context: Context, client: WebClient): FunctionFailFn {
+    const token = selectToken(context);
+
+    return (params: Parameters<FunctionFailFn>[0]) => {
+      const { error } = params ?? {};
+      const { functionExecutionId } = context;
+
+      return client.functions.completeError({
+        token,
+        error,
+        function_execution_id: functionExecutionId,
+      });
+    };
+  }
 }
 
 /** Helper Functions */
 
-export function validate(callbackId: string, listeners: WorkflowFunctionMiddleware): void {
+export function validate(callbackId: string, listeners: CustomFunctionMiddleware): void {
   // Ensure callbackId is valid
   if (typeof callbackId !== 'string') {
-    const errorMsg = 'WorkflowFunction expects a callback_id as the first argument';
-    throw new WorkflowFunctionInitializationError(errorMsg);
+    const errorMsg = 'CustomFunction expects a callback_id as the first argument';
+    throw new CustomFunctionInitializationError(errorMsg);
   }
 
   // Ensure all listeners are functions
   listeners.forEach((listener) => {
     if (!(listener instanceof Function)) {
-      const errorMsg = 'All WorkflowFunction listeners must be functions';
-      throw new WorkflowFunctionInitializationError(errorMsg);
+      const errorMsg = 'All CustomFunction listeners must be functions';
+      throw new CustomFunctionInitializationError(errorMsg);
     }
   });
 }
@@ -119,8 +153,8 @@ export function validate(callbackId: string, listeners: WorkflowFunctionMiddlewa
  * @param args workflow_step_edit action
  */
 export async function processStepMiddleware(
-  args: AllWorkflowFunctionMiddlewareArgs,
-  middleware: WorkflowFunctionMiddleware,
+  args: AllCustomFunctionMiddlewareArgs,
+  middleware: CustomFunctionMiddleware,
 ): Promise<void> {
   const { context, client, logger } = args;
   const callbacks = [...middleware] as Middleware<AnyMiddlewareArgs>[];
@@ -134,59 +168,13 @@ export async function processStepMiddleware(
   }
 }
 
-export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllWorkflowFunctionMiddlewareArgs {
+function isFunctionEvent(args: AnyMiddlewareArgs): args is AllCustomFunctionMiddlewareArgs {
   return VALID_PAYLOAD_TYPES.has(args.payload.type);
 }
 
 function selectToken(context: Context): string | undefined {
   // If attachFunctionToken = false, fallback to botToken or userToken
-  return context.functionBotToken ? context.functionBotToken : context.botToken || context.userToken;
-}
-
-/**
- * Factory for `complete()` utility
- * @param args function_executed event
- */
-export function createFunctionComplete(
-  args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
-): FunctionCompleteFn {
-  const {
-    context,
-    client,
-    payload: { function_execution_id },
-  } = args;
-  const token = selectToken(context);
-
-  return (params: Parameters<FunctionCompleteFn>[0] = {}) => client.functions.completeSuccess({
-    token,
-    outputs: params.outputs || {},
-    function_execution_id,
-  });
-}
-
-/**
- * Factory for `fail()` utility
- * @param args function_executed event
- */
-export function createFunctionFail(
-  args: AllWorkflowFunctionMiddlewareArgs<WorkflowFunctionExecuteMiddlewareArgs>,
-): FunctionFailFn {
-  const {
-    context,
-    client,
-    payload: { function_execution_id },
-  } = args;
-  const token = selectToken(context);
-
-  return (params: Parameters<FunctionFailFn>[0]) => {
-    const { error } = params ?? {};
-
-    return client.functions.completeError({
-      token,
-      error,
-      function_execution_id,
-    });
-  };
+  return context.functionBotAccessToken ? context.functionBotAccessToken : context.botToken || context.userToken;
 }
 
 /**
@@ -195,20 +183,20 @@ export function createFunctionFail(
  *    - events will *not* continue down global middleware chain to subsequent listeners
  *  2. augments args with step lifecycle-specific properties/utilities
  * */
-export function prepareFunctionArgs(args: any): AllWorkflowFunctionMiddlewareArgs {
+function prepareFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs {
   const { next: _next, ...functionArgs } = args;
   const preparedArgs: any = { ...functionArgs };
   const token = selectToken(functionArgs.context);
 
-  // Making calls with a functionBotToken establishes continuity between
+  // Making calls with a functionBotAccessToken establishes continuity between
   // a function_executed event and subsequent interactive events (actions)
   const client = new WebClient(token, { ...functionArgs.client });
   preparedArgs.client = client;
 
   // Utility args
   preparedArgs.inputs = preparedArgs.event.inputs;
-  preparedArgs.complete = createFunctionComplete(preparedArgs);
-  preparedArgs.fail = createFunctionFail(preparedArgs);
+  preparedArgs.complete = CustomFunction.createFunctionComplete(preparedArgs.context, client);
+  preparedArgs.fail = CustomFunction.createFunctionFail(preparedArgs.context, client);
 
   return preparedArgs;
 }
diff --git a/src/errors.ts b/src/errors.ts
index 3f7db700b..f5a1505cb 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -35,7 +35,7 @@ export enum ErrorCode {
 
   WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error',
 
-  WorkflowFunctionInitializationError = 'slack_bolt_workflow_function_initialization_error',
+  CustomFunctionInitializationError = 'slack_bolt_workflow_function_initialization_error',
 }
 
 export class UnknownError extends Error implements CodedError {
@@ -141,6 +141,6 @@ export class WorkflowStepInitializationError extends Error implements CodedError
   public code = ErrorCode.WorkflowStepInitializationError;
 }
 
-export class WorkflowFunctionInitializationError extends Error implements CodedError {
-  public code = ErrorCode.WorkflowFunctionInitializationError;
+export class CustomFunctionInitializationError extends Error implements CodedError {
+  public code = ErrorCode.CustomFunctionInitializationError;
 }
diff --git a/src/types/events/base-events.ts b/src/types/events/base-events.ts
index cb9cd793d..8129a584e 100644
--- a/src/types/events/base-events.ts
+++ b/src/types/events/base-events.ts
@@ -427,37 +427,39 @@ export interface FileUnsharedEvent {
 }
 
 export interface FunctionParams {
-  type?: string,
-  name?: string,
-  description?: string,
-  title?: string,
-  is_required?: boolean,
+  type?: string;
+  name?: string;
+  description?: string;
+  title?: string;
+  is_required?: boolean;
 }
 
-export interface FunctionInputValues {
+export interface FunctionInputs {
   [key: string]: unknown;
 }
 
-export type FunctionOutputValues = FunctionInputValues;
+export type FunctionOutputValues = FunctionInputs;
 
-// TODO :: Update this with new payload info
 export interface FunctionExecutedEvent {
-  type: 'function_executed',
+  type: 'function_executed';
   function: {
-    id: string,
-    callback_id: string,
-    title: string,
-    description: string,
-    type: string,
-    input_parameters: FunctionParams[],
-    output_parameters: FunctionParams[],
-    app_id: string,
-    date_updated: number,
-  },
-  function_execution_id: string,
-  inputs: FunctionInputValues,
-  workflow_token: string, // xwfp-xxxxxxxxxxx
-  event_ts: string,
+    id: string;
+    callback_id: string;
+    title: string;
+    description: string;
+    type: string;
+    input_parameters: FunctionParams[];
+    output_parameters: FunctionParams[];
+    app_id: string;
+    date_created: number;
+    date_updated: number;
+    date_deleted: number
+  };
+  inputs: FunctionInputs;
+  function_execution_id: string;
+  workflow_execution_id: string;
+  event_ts: string;
+  bot_access_token: string;
 }
 
 export interface GridMigrationFinishedEvent {

From 630f188d83dfe07a1f1b0a34bc6aebde15fe3660 Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Thu, 25 Jan 2024 15:24:07 -0800
Subject: [PATCH 07/13] Add CustomFunction test + adjust naming

---
 src/CustomFunction.spec.ts | 237 +++++++++++++++++++++++++++++++++++++
 src/CustomFunction.ts      |  63 +++++-----
 2 files changed, 273 insertions(+), 27 deletions(-)
 create mode 100644 src/CustomFunction.spec.ts

diff --git a/src/CustomFunction.spec.ts b/src/CustomFunction.spec.ts
new file mode 100644
index 000000000..5b5a5429f
--- /dev/null
+++ b/src/CustomFunction.spec.ts
@@ -0,0 +1,237 @@
+import 'mocha';
+import { assert } from 'chai';
+import sinon from 'sinon';
+import rewiremock from 'rewiremock';
+import {
+  CustomFunction,
+  SlackCustomFunctionMiddlewareArgs,
+  AllCustomFunctionMiddlewareArgs,
+  CustomFunctionMiddleware,
+  CustomFunctionExecuteMiddlewareArgs,
+} from './CustomFunction';
+import { Override } from './test-helpers';
+import { AllMiddlewareArgs, AnyMiddlewareArgs, Middleware } from './types';
+import { CustomFunctionInitializationError } from './errors';
+
+async function importCustomFunction(overrides: Override = {}): Promise<typeof import('./CustomFunction')> {
+  return rewiremock.module(() => import('./CustomFunction'), overrides);
+}
+
+const MOCK_FN = async () => {};
+const MOCK_FN_2 = async () => {};
+
+const MOCK_MIDDLEWARE_SINGLE = [MOCK_FN];
+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);
+      assert.isNotNull(fn);
+    });
+
+    it('should accept multiple functions as middleware', async () => {
+      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 middleware = fn.getMiddleware();
+      const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
+        SlackCustomFunctionMiddlewareArgs & AllMiddlewareArgs;
+
+      const fakeNext = sinon.spy();
+      fakeEditArgs.next = fakeNext;
+
+      await middleware(fakeEditArgs);
+
+      assert(fakeNext.notCalled);
+    });
+
+    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 middleware = fn.getMiddleware();
+      const fakeEditArgs = createFakeFunctionExecutedEvent() as unknown as
+        SlackCustomFunctionMiddlewareArgs & 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 fn = new CustomFunction('test_view_callback_id', MOCK_MIDDLEWARE_SINGLE);
+      const middleware = fn.getMiddleware();
+      const fakeViewArgs = createFakeViewEvent() as unknown as
+        SlackCustomFunctionMiddlewareArgs & 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 } = await importCustomFunction();
+
+      // intentionally casting to string to trigger failure
+      const badId = {} as string;
+      const validationFn = () => validate(badId, MOCK_MIDDLEWARE_SINGLE);
+
+      const expectedMsg = 'CustomFunction expects a callback_id as the first argument';
+      assert.throws(validationFn, CustomFunctionInitializationError, expectedMsg);
+    });
+
+    it('should throw an error if middleware is not a function or array', async () => {
+      const { validate } = await importCustomFunction();
+
+      // intentionally casting to CustomFunctionMiddleware to trigger failure
+      const badConfig = '' as unknown as CustomFunctionMiddleware;
+
+      const validationFn = () => validate('callback_id', badConfig);
+      const expectedMsg = 'CustomFunction expects a function or array of functions as the second argument';
+      assert.throws(validationFn, CustomFunctionInitializationError, expectedMsg);
+    });
+
+    it('should throw an error if middleware is not a single callback or an array of callbacks', async () => {
+      const { validate } = await importCustomFunction();
+
+      // intentionally casting to CustomFunctionMiddleware to trigger failure
+      const badMiddleware = [
+        async () => {},
+        'not-a-function',
+      ] as unknown as CustomFunctionMiddleware;
+
+      const validationFn = () => validate('callback_id', badMiddleware);
+      const expectedMsg = 'All CustomFunction middleware must be functions';
+      assert.throws(validationFn, CustomFunctionInitializationError, expectedMsg);
+    });
+  });
+
+  describe('isFunctionEvent', () => {
+    it('should return true if recognized function_executed payload type', async () => {
+      const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as SlackCustomFunctionMiddlewareArgs
+      & AllMiddlewareArgs;
+
+      const { isFunctionEvent } = await importCustomFunction();
+      const eventIsFunctionExcuted = isFunctionEvent(fakeExecuteArgs);
+
+      assert.isTrue(eventIsFunctionExcuted);
+    });
+
+    it('should return false if not a function_executed payload type', async () => {
+      const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
+      fakeExecutedEvent.payload.type = 'invalid_type';
+
+      const { isFunctionEvent } = await importCustomFunction();
+      const eventIsFunctionExecuted = isFunctionEvent(fakeExecutedEvent);
+
+      assert.isFalse(eventIsFunctionExecuted);
+    });
+  });
+
+  describe('enrichFunctionArgs', () => {
+    it('should remove next() from all original event args', async () => {
+      const fakeExecutedEvent = createFakeFunctionExecutedEvent() as unknown as AnyMiddlewareArgs;
+
+      const { enrichFunctionArgs } = await importCustomFunction();
+      const executeFunctionArgs = enrichFunctionArgs(fakeExecutedEvent);
+
+      assert.notExists(executeFunctionArgs.next);
+    });
+
+    it('should augment function_executed args with inputs, complete, and fail', async () => {
+      const fakeArgs = createFakeFunctionExecutedEvent();
+
+      const { enrichFunctionArgs } = await importCustomFunction();
+      const functionArgs = enrichFunctionArgs(fakeArgs);
+
+      assert.exists(functionArgs.inputs);
+      assert.exists(functionArgs.complete);
+      assert.exists(functionArgs.fail);
+    });
+  });
+
+  describe('custom function utility functions', () => {
+    it('complete should call functions.completeSuccess', async () => {
+      // TODO
+    });
+
+    it('fail should call functions.completeError', async () => {
+      // TODO
+    });
+
+    it('inputs should map to function payload inputs', async () => {
+      const fakeExecuteArgs = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
+
+      const { enrichFunctionArgs } = await importCustomFunction();
+      const enrichedArgs = enrichFunctionArgs(fakeExecuteArgs);
+
+      assert.isTrue(enrichedArgs.inputs === fakeExecuteArgs.event.inputs);
+    });
+  });
+
+  describe('processFunctionMiddleware', () => {
+    it('should call each callback in user-provided middleware', async () => {
+      const { ...fakeArgs } = createFakeFunctionExecutedEvent() as unknown as AllCustomFunctionMiddlewareArgs;
+      const { processFunctionMiddleware } = await importCustomFunction();
+
+      const fn1 = sinon.spy((async ({ next: continuation }) => {
+        await continuation();
+      }) as Middleware<CustomFunctionExecuteMiddlewareArgs>);
+      const fn2 = sinon.spy(async () => {});
+      const fakeMiddleware = [fn1, fn2] as CustomFunctionMiddleware;
+
+      await processFunctionMiddleware(fakeArgs, fakeMiddleware);
+
+      assert(fn1.called);
+      assert(fn2.called);
+    });
+  });
+});
+
+function createFakeFunctionExecutedEvent() {
+  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',
+    },
+    context: {
+      functionBotAccessToken: 'xwfp-123',
+    },
+  };
+}
+
+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: {},
+  };
+}
diff --git a/src/CustomFunction.ts b/src/CustomFunction.ts
index b20276535..8f52c7e97 100644
--- a/src/CustomFunction.ts
+++ b/src/CustomFunction.ts
@@ -10,6 +10,7 @@ import {
   AnyMiddlewareArgs,
   SlackEventMiddlewareArgs,
   Context,
+  FunctionExecutedEvent,
 } from './types';
 import processMiddleware from './middleware/process';
 import { CustomFunctionInitializationError } from './errors';
@@ -35,6 +36,7 @@ export interface FunctionFailFn {
 }
 
 export interface CustomFunctionExecuteMiddlewareArgs extends SlackEventMiddlewareArgs<'function_executed'> {
+  inputs: FunctionExecutedEvent['inputs'];
   complete: FunctionCompleteFn;
   fail: FunctionFailFn;
 }
@@ -43,9 +45,9 @@ export interface CustomFunctionExecuteMiddlewareArgs extends SlackEventMiddlewar
 
 export type SlackCustomFunctionMiddlewareArgs = CustomFunctionExecuteMiddlewareArgs;
 
-type CustomFunctionExecuteMiddleware = Middleware<CustomFunctionExecuteMiddlewareArgs>;
+type CustomFunctionExecuteMiddleware = Middleware<CustomFunctionExecuteMiddlewareArgs>[];
 
-export type CustomFunctionMiddleware = CustomFunctionExecuteMiddleware[];
+export type CustomFunctionMiddleware = Middleware<CustomFunctionExecuteMiddlewareArgs>[];
 
 export type AllCustomFunctionMiddlewareArgs
   <T extends SlackCustomFunctionMiddlewareArgs = SlackCustomFunctionMiddlewareArgs> = T & AllMiddlewareArgs;
@@ -64,7 +66,7 @@ export class CustomFunction {
 
   public constructor(
     callbackId: string,
-    middleware: CustomFunctionMiddleware,
+    middleware: CustomFunctionExecuteMiddleware,
   ) {
     validate(callbackId, middleware);
 
@@ -86,12 +88,12 @@ export class CustomFunction {
   }
 
   private async processEvent(args: AllCustomFunctionMiddlewareArgs): Promise<void> {
-    const functionArgs = prepareFunctionArgs(args);
-    const stepMiddleware = this.getStepMiddleware();
-    return processStepMiddleware(functionArgs, stepMiddleware);
+    const functionArgs = enrichFunctionArgs(args);
+    const functionMiddleware = this.getFunctionMiddleware();
+    return processFunctionMiddleware(functionArgs, functionMiddleware);
   }
 
-  private getStepMiddleware(): CustomFunctionMiddleware {
+  private getFunctionMiddleware(): CustomFunctionMiddleware {
     return this.middleware;
   }
 
@@ -131,28 +133,35 @@ export class CustomFunction {
 }
 
 /** Helper Functions */
-
-export function validate(callbackId: string, listeners: CustomFunctionMiddleware): void {
+export function validate(callbackId: string, middleware: CustomFunctionExecuteMiddleware): void {
   // Ensure callbackId is valid
   if (typeof callbackId !== 'string') {
     const errorMsg = 'CustomFunction expects a callback_id as the first argument';
     throw new CustomFunctionInitializationError(errorMsg);
   }
 
-  // Ensure all listeners are functions
-  listeners.forEach((listener) => {
-    if (!(listener instanceof Function)) {
-      const errorMsg = 'All CustomFunction listeners must be functions';
-      throw new CustomFunctionInitializationError(errorMsg);
-    }
-  });
+  // Ensure middleware argument is either a function or an array
+  if (typeof middleware !== 'function' && !Array.isArray(middleware)) {
+    const errorMsg = 'CustomFunction expects a function or array of functions as the second argument';
+    throw new CustomFunctionInitializationError(errorMsg);
+  }
+
+  // Ensure array includes only functions
+  if (Array.isArray(middleware)) {
+    middleware.forEach((fn) => {
+      if (!(fn instanceof Function)) {
+        const errorMsg = 'All CustomFunction middleware must be functions';
+        throw new CustomFunctionInitializationError(errorMsg);
+      }
+    });
+  }
 }
 
 /**
- * `processStepMiddleware()` invokes each callback for lifecycle event
+ * `processFunctionMiddleware()` invokes each callback for lifecycle event
  * @param args workflow_step_edit action
  */
-export async function processStepMiddleware(
+export async function processFunctionMiddleware(
   args: AllCustomFunctionMiddlewareArgs,
   middleware: CustomFunctionMiddleware,
 ): Promise<void> {
@@ -168,7 +177,7 @@ export async function processStepMiddleware(
   }
 }
 
-function isFunctionEvent(args: AnyMiddlewareArgs): args is AllCustomFunctionMiddlewareArgs {
+export function isFunctionEvent(args: AnyMiddlewareArgs): args is AllCustomFunctionMiddlewareArgs {
   return VALID_PAYLOAD_TYPES.has(args.payload.type);
 }
 
@@ -178,25 +187,25 @@ function selectToken(context: Context): string | undefined {
 }
 
 /**
- * `prepareFunctionArgs()` takes in a function's args and:
+ * `enrichFunctionArgs()` takes in a function'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
  * */
-function prepareFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs {
+export function enrichFunctionArgs(args: any): AllCustomFunctionMiddlewareArgs {
   const { next: _next, ...functionArgs } = args;
-  const preparedArgs: any = { ...functionArgs };
+  const enrichedArgs: any = { ...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 });
-  preparedArgs.client = client;
+  enrichedArgs.client = client;
 
   // Utility args
-  preparedArgs.inputs = preparedArgs.event.inputs;
-  preparedArgs.complete = CustomFunction.createFunctionComplete(preparedArgs.context, client);
-  preparedArgs.fail = CustomFunction.createFunctionFail(preparedArgs.context, client);
+  enrichedArgs.inputs = enrichedArgs.event.inputs;
+  enrichedArgs.complete = CustomFunction.createFunctionComplete(enrichedArgs.context, client);
+  enrichedArgs.fail = CustomFunction.createFunctionFail(enrichedArgs.context, client);
 
-  return preparedArgs;
+  return enrichedArgs;
 }

From caec4b1356f8b584e2a6e677ef89bb1156ba94ad Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Thu, 25 Jan 2024 15:33:16 -0800
Subject: [PATCH 08/13] Update error description

---
 src/errors.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/errors.ts b/src/errors.ts
index f5a1505cb..19e54e39a 100644
--- a/src/errors.ts
+++ b/src/errors.ts
@@ -35,7 +35,7 @@ export enum ErrorCode {
 
   WorkflowStepInitializationError = 'slack_bolt_workflow_step_initialization_error',
 
-  CustomFunctionInitializationError = 'slack_bolt_workflow_function_initialization_error',
+  CustomFunctionInitializationError = 'slack_bolt_custom_function_initialization_error',
 }
 
 export class UnknownError extends Error implements CodedError {

From 247b4c759102ae003ff66221441113f2be7cc8eb Mon Sep 17 00:00:00 2001
From: Ethan Zimbelman <ethan.zimbelman@me.com>
Date: Thu, 25 Jan 2024 16:20:52 -0800
Subject: [PATCH 09/13] Bump the @slack/web-api version to v6.12.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 9312fd59c..00f062233 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,7 @@
     "@slack/oauth": "^2.6.2",
     "@slack/socket-mode": "^1.3.3",
     "@slack/types": "^2.11.0",
-    "@slack/web-api": "^6.11.2",
+    "@slack/web-api": "^6.12.0",
     "@types/express": "^4.16.1",
     "@types/promise.allsettled": "^1.0.3",
     "@types/tsscmp": "^1.0.0",

From 11a42c09b5ef81b98c26896bdb0b3a6d0cc267b1 Mon Sep 17 00:00:00 2001
From: Ethan Zimbelman <ethan.zimbelman@me.com>
Date: Thu, 25 Jan 2024 16:25:10 -0800
Subject: [PATCH 10/13] v3.17.1-customFunctionBeta.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 00f062233..68df7d8a3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@slack/bolt",
-  "version": "3.17.1",
+  "version": "3.17.1-customFunctionBeta.0",
   "description": "A framework for building Slack apps, fast.",
   "author": "Slack Technologies, LLC",
   "license": "MIT",

From bea900d20c488e81419e5563feb5c8e9469110d7 Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Mon, 12 Aug 2024 19:20:01 +0000
Subject: [PATCH 11/13] Fixes and polish for stable release (#2128)

Co-authored-by: Fil Maj <maj.fil@gmail.com>
---
 package.json                    |   3 +-
 src/App.ts                      |  22 +++--
 src/CustomFunction.spec.ts      | 144 +++++++++++++++++++++++---------
 src/CustomFunction.ts           |  36 +++++---
 src/errors.ts                   |  10 +++
 src/middleware/builtin.ts       |  24 ++++--
 src/middleware/process.ts       |   2 +-
 src/types/actions/index.ts      |   7 +-
 src/types/events/base-events.ts |   8 +-
 src/types/events/index.ts       |   8 +-
 src/types/helpers.ts            |   2 +
 src/types/middleware.ts         |  22 ++++-
 tsconfig.eslint.json            |   2 +-
 types-tests/event.test-d.ts     |  20 ++---
 14 files changed, 222 insertions(+), 88 deletions(-)

diff --git a/package.json b/package.json
index 2ac6247ae..40c180517 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 9902de838..ac4cbefd7 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 5b5a5429f..24dc00059 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 8f52c7e97..4033bc0d9 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 c270b9c35..09a5a8b7f 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 69f908c12..6152c3af4 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 b183f46b6..cc963d8f3 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 c94a8673c..5c7e7841a 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 32d9ae528..9abc99558 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 b888b4a00..0b2c0ef47 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 3a7a8bb9d..124f8423b 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 a36fddc62..d1d8878df 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 776c2cfb7..d554c4b36 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/**/*"
   ]
 }
diff --git a/types-tests/event.test-d.ts b/types-tests/event.test-d.ts
index b24a6687a..06a27ab8f 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);
-  })
+  }),
 );

From 23ef516e96ec32fbb85c904e8001e34a5b8ab6ad Mon Sep 17 00:00:00 2001
From: Alissa Renz <alissa.renz@gmail.com>
Date: Wed, 14 Aug 2024 12:02:02 +0000
Subject: [PATCH 12/13] [Bug] Fix chained function actions (#2200)

---
 src/App.ts | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/App.ts b/src/App.ts
index ac4cbefd7..184063f81 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -976,6 +976,7 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
       if (functionInputs) { context.functionInputs = functionInputs; }
     }
 
+    // Attach and make available the JIT/function-related token on context
     if (this.attachFunctionToken) {
       if (functionBotAccessToken) { context.functionBotAccessToken = functionBotAccessToken; }
     }
@@ -1091,7 +1092,11 @@ export default class App<AppCustomContext extends StringIndexed = StringIndexed>
 
     // Get the client arg
     let { client } = this;
-    const token = selectToken(context);
+
+    // If functionBotAccessToken exists on context, the incoming event is function-related *and* the
+    // user has `attachFunctionToken` enabled. In that case, subsequent calls with the client should
+    // use the function-related/JIT token in lieu of the botToken or userToken.
+    const token = context.functionBotAccessToken ? context.functionBotAccessToken : selectToken(context);
 
     // Add complete() and fail() utilities for function-related interactivity
     if (type === IncomingEventType.Action && context.functionExecutionId !== undefined) {

From 90ce8120d98267b6a2c11f9152ab549d194d9cda Mon Sep 17 00:00:00 2001
From: Fil Maj <maj.fil@gmail.com>
Date: Wed, 14 Aug 2024 16:00:57 +0000
Subject: [PATCH 13/13] Remote functions: typescript integration test with
 bolt-ts-starter-template (#2202)

---
 .github/workflows/samples.yml | 41 +++++++++++++++++++++++++++++++++++
 1 file changed, 41 insertions(+)
 create mode 100644 .github/workflows/samples.yml

diff --git a/.github/workflows/samples.yml b/.github/workflows/samples.yml
new file mode 100644
index 000000000..9d0a0f2ba
--- /dev/null
+++ b/.github/workflows/samples.yml
@@ -0,0 +1,41 @@
+# This workflow runs a TypeScript compilation against slack sample apps built on top of bolt-js
+name: Samples Integration Type-checking
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+
+jobs:
+  samples:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [18.x, 20.x, 22.x]
+        sample:
+          - slack-samples/bolt-ts-starter-template
+
+    steps:
+      - name: Use Node.js ${{ matrix.node-version }}
+        uses: actions/setup-node@v4
+        with:
+          node-version: ${{ matrix.node-version }}
+      - name: Checkout bolt-js
+        uses: actions/checkout@v4
+        with:
+          path: ./bolt-js
+      - name: Install and link bolt-js
+        working-directory: ./bolt-js
+        run: npm i && npm link .
+      - name: Checkout ${{ matrix.sample }}
+        uses: actions/checkout@v4
+        with:
+          repository: ${{ matrix.sample }}
+          path: ./sample
+      - name: Install sample dependencies and link bolt-js
+        working-directory: ./sample
+        run: npm i && npm link @slack/bolt
+      - name: Compile sample
+        working-directory: ./sample
+        run: npx tsc
+