Repository: slackapi/bolt-js (ts)

Issue:
Add types of state.values on modal submission
### Description

I am currently working on validating modal submissions from Slack. Somewhat unfortunately all block types carry their value under different keys (e.g. rather than all having a `value` key there are `selected_users`, `selected_channel`, `selected_options` and more).

So far I have been unable to find documentation about the variety of response type structures that input blocks can have on modal submission.

I noticed there is a TODO comment here: https://github.com/slackapi/bolt-js/blob/2ab64673819a81608e8e9352ceb53a01158accdd/src/types/view/index.ts#L87

I am happy to submit a PR with a union type for this response if someone can point me to docs on what is available from the API. For my own bot, I have had to step through with a debugger to introspect the responses.

As a follow up, it would be _amazing_ if there was a way to get simplified response schema from the slack's response (e.g. get a `block_id: value` mapping or some relative equivalent that includes the `action_id`). This would make it much easier to quickly write any schema validation using tools like https://github.com/jquense/yup or https://hapi.dev/module/joi/ without having to massage the data into a flatter form.

Describe your issue here.

### What type of issue is this? (place an `x` in one of the `[ ]`)
- [ ] bug
- [X] enhancement (feature request)
- [ ] question
- [ ] documentation related
- [ ] testing related
- [ ] discussion

### Requirements (place an `x` in each of the `[ ]`)
* [X] I've read and understood the [Contributing guidelines](https://github.com/slackapi/bolt/blob/master/.github/contributing.md) and have done my best effort to follow them.
* [X] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).
* [X] I've searched for any related issues and avoided creating a duplicate issue.


Pull request:
Fix #497 Add types of state.values on modal submission
###  Summary

This pull request resolves #497 by adding complete types for the elements in `view.state.values` in `view_submission` payloads

### Requirements (place an `x` in each `[ ]`)

* [x] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/bolt/blob/main/.github/contributing.md) and have done my best effort to follow them.
* [x] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct).

Patch:
diff --git a/src/App.ts b/src/App.ts
index ccdacac..05cb494 100644
--- a/src/App.ts
+++ b/src/App.ts
@@ -571,14 +571,16 @@ export default class App {
     this.listeners.push([onlyCommands, matchCommandName(commandName), ...listeners] as Middleware<AnyMiddlewareArgs>[]);
   }
 
-  public options<Source extends OptionsSource = OptionsSource>(
+  public options<Source extends OptionsSource = 'block_suggestion'>(
     actionId: string | RegExp,
     ...listeners: Middleware<SlackOptionsMiddlewareArgs<Source>>[]
   ): void;
+  // TODO: reflect the type in constraits to Source
   public options<Source extends OptionsSource = OptionsSource>(
     constraints: ActionConstraints,
     ...listeners: Middleware<SlackOptionsMiddlewareArgs<Source>>[]
   ): void;
+  // TODO: reflect the type in constraits to Source
   public options<Source extends OptionsSource = OptionsSource>(
     actionIdOrConstraints: string | RegExp | ActionConstraints,
     ...listeners: Middleware<SlackOptionsMiddlewareArgs<Source>>[]
diff --git a/src/receivers/ExpressReceiver.spec.ts b/src/receivers/ExpressReceiver.spec.ts
index 7ac7957..5b385b5 100644
--- a/src/receivers/ExpressReceiver.spec.ts
+++ b/src/receivers/ExpressReceiver.spec.ts
@@ -383,7 +383,11 @@ describe('ExpressReceiver', function () {
     // ----------------------------
     // runWithValidRequest
 
-    async function runWithValidRequest(req: Request, state: any): Promise<void> {
+    async function runWithValidRequest(
+      req: Request,
+      state: any,
+      signingSecretFn?: () => PromiseLike<string>,
+    ): Promise<void> {
       // Arrange
       const resp = buildResponseToVerify(state);
       const next = (error: any) => {
@@ -391,7 +395,7 @@ describe('ExpressReceiver', function () {
       };
 
       // Act
-      const verifier = verifySignatureAndParseRawBody(noopLogger, signingSecret);
+      const verifier = verifySignatureAndParseRawBody(noopLogger, signingSecretFn || signingSecret);
       // eslint-disable-next-line @typescript-eslint/await-thenable
       await verifier(req, resp, next);
     }
@@ -410,6 +414,13 @@ describe('ExpressReceiver', function () {
       assert.isUndefined(state.error);
     });
 
+    it('should verify requests on GCP using async signingSecret', async () => {
+      const state: any = {};
+      await runWithValidRequest(buildGCPRequest(), state, () => Promise.resolve(signingSecret));
+      // Assert
+      assert.isUndefined(state.error);
+    });
+
     // ----------------------------
     // parse error
 
diff --git a/src/receivers/ExpressReceiver.ts b/src/receivers/ExpressReceiver.ts
index 8409f45..6a541c1 100644
--- a/src/receivers/ExpressReceiver.ts
+++ b/src/receivers/ExpressReceiver.ts
@@ -18,7 +18,7 @@ import { renderHtmlForInstallPath } from './render-html-for-install-path';
 // TODO: we throw away the key names for endpoints, so maybe we should use this interface. is it better for migrations?
 // if that's the reason, let's document that with a comment.
 export interface ExpressReceiverOptions {
-  signingSecret: string;
+  signingSecret: string | (() => PromiseLike<string>);
   logger?: Logger;
   logLevel?: LogLevel;
   endpoints?:
@@ -319,7 +319,10 @@ export const respondToUrlVerification: RequestHandler = (req, res, next) => {
  * - Verify the request signature
  * - Parse request.body and assign the successfully parsed object to it.
  */
-export function verifySignatureAndParseRawBody(logger: Logger, signingSecret: string): RequestHandler {
+export function verifySignatureAndParseRawBody(
+  logger: Logger,
+  signingSecret: string | (() => PromiseLike<string>),
+): RequestHandler {
   return async (req, res, next) => {
     let stringBody: string;
     // On some environments like GCP (Google Cloud Platform),
@@ -338,7 +341,11 @@ export function verifySignatureAndParseRawBody(logger: Logger, signingSecret: st
     try {
       // This handler parses `req.body` or `req.rawBody`(on Google Could Platform)
       // and overwrites `req.body` with the parsed JS object.
-      req.body = verifySignatureAndParseBody(signingSecret, stringBody, req.headers);
+      req.body = verifySignatureAndParseBody(
+        typeof signingSecret === 'string' ? signingSecret : await signingSecret(),
+        stringBody,
+        req.headers,
+      );
     } catch (error) {
       if (error) {
         if (error instanceof ReceiverAuthenticityError) {
diff --git a/src/types/view/index.ts b/src/types/view/index.ts
index 7805fa3..83f7d37 100644
--- a/src/types/view/index.ts
+++ b/src/types/view/index.ts
@@ -1,4 +1,4 @@
-import { View } from '@slack/types';
+import { View, PlainTextElement } from '@slack/types';
 import { StringIndexed } from '../helpers';
 import { AckFn } from '../utilities';
 
@@ -116,10 +116,31 @@ export interface ViewWorkflowStepClosedAction extends ViewClosedAction {
   };
 }
 
+export interface ViewStateSelectedOption {
+  text: PlainTextElement;
+  value: string;
+}
+
+export interface ViewStateValue {
+  type: string;
+  value?: string | null;
+  selected_date?: string | null;
+  selected_time?: string | null;
+  selected_conversation?: string | null;
+  selected_channel?: string | null;
+  selected_user?: string | null;
+  selected_option?: ViewStateSelectedOption | null;
+  selected_conversations?: string[];
+  selected_channels?: string[];
+  selected_users?: string[];
+  selected_options?: ViewStateSelectedOption[];
+}
+
 export interface ViewOutput {
   id: string;
   callback_id: string;
   team_id: string;
+  app_installed_team_id?: string;
   app_id: string | null;
   bot_id: string;
   title: PlainTextElementOutput;
@@ -130,7 +151,7 @@ export interface ViewOutput {
   state: {
     values: {
       [blockId: string]: {
-        [actionId: string]: any; // TODO: a union of all the input elements' output payload
+        [actionId: string]: ViewStateValue;
       };
     };
   };


Test patch:
diff --git a/types-tests/options.test-d.ts b/types-tests/options.test-d.ts
index 228b45c..8bfc2b1 100644
--- a/types-tests/options.test-d.ts
+++ b/types-tests/options.test-d.ts
@@ -1,38 +1,63 @@
-import { expectType, expectNotType } from 'tsd';
+import { expectType, expectNotType, expectError } from 'tsd';
 import { App, OptionsRequest, OptionsSource } from '../dist';
+import { Option } from '@slack/types';
 
 const app = new App({ token: 'TOKEN', signingSecret: 'Signing Secret' });
 
-expectType<void>(app.options('action-id-or-callback-id', async ({ options }) => {
-  expectType<OptionsRequest<OptionsSource>>(options);
+const blockSuggestionOptions: Option[] = [
+  {
+    "text": {
+      "type": "plain_text",
+      "text": "foo"
+    },
+    "value": "bar"
+  }
+];
+
+// set the default to block_suggestion
+expectType<void>(app.options('action-id-or-callback-id', async ({ options, ack }) => {
+  expectType<OptionsRequest<'block_suggestion'>>(options);
+  expectType<never>(options.callback_id);
+  options.block_id;
+  options.action_id;
+  // https://github.com/slackapi/bolt-js/issues/720
+  await ack({ options: blockSuggestionOptions });
   await Promise.resolve(options);
 }));
 
 // block_suggestion
-expectType<void>(app.options<'block_suggestion'>({ action_id: 'a' }, async ({ options }) => {
+expectType<void>(app.options<'block_suggestion'>({ action_id: 'a' }, async ({ options, ack }) => {
   expectNotType<OptionsRequest<OptionsSource>>(options);
   expectType<OptionsRequest<'block_suggestion'>>(options);
+  // https://github.com/slackapi/bolt-js/issues/720
+  await ack({ options: blockSuggestionOptions });
   await Promise.resolve(options);
 }));
 // FIXME: app.options({ type: 'block_suggestion', action_id: 'a' } does not work
 
 // interactive_message (attachments)
-expectType<void>(app.options<'interactive_message'>({ callback_id: 'a' }, async ({ options }) => {
+expectType<void>(app.options<'interactive_message'>({ callback_id: 'a' }, async ({ options, ack }) => {
   expectNotType<OptionsRequest<OptionsSource>>(options);
   expectType<OptionsRequest<'interactive_message'>>(options);
+  // https://github.com/slackapi/bolt-js/issues/720
+  expectError(ack({ options: blockSuggestionOptions }));
   await Promise.resolve(options);
 }));
 
-expectType<void>(app.options({ type: 'interactive_message', callback_id: 'a' }, async ({ options }) => {
+expectType<void>(app.options({ type: 'interactive_message', callback_id: 'a' }, async ({ options, ack }) => {
   // FIXME: the type should be OptionsRequest<'interactive_message'>
   expectType<OptionsRequest<OptionsSource>>(options);
+  // https://github.com/slackapi/bolt-js/issues/720
+  expectError(ack({ options: blockSuggestionOptions }));
   await Promise.resolve(options);
 }));
 
 // dialog_suggestion (dialog)
-expectType<void>(app.options<'dialog_suggestion'>({ callback_id: 'a' }, async ({ options }) => {
+expectType<void>(app.options<'dialog_suggestion'>({ callback_id: 'a' }, async ({ options, ack }) => {
   expectNotType<OptionsRequest<OptionsSource>>(options);
   expectType<OptionsRequest<'dialog_suggestion'>>(options);
+  // https://github.com/slackapi/bolt-js/issues/720
+  expectError(ack({ options: blockSuggestionOptions }));
   await Promise.resolve(options);
 }));
 // FIXME: app.options({ type: 'dialog_suggestion', callback_id: 'a' } does not work
diff --git a/types-tests/view.test-d.ts b/types-tests/view.test-d.ts
index 72d80c0..721e633 100644
--- a/types-tests/view.test-d.ts
+++ b/types-tests/view.test-d.ts
@@ -31,3 +31,70 @@ expectType<void>(
     await Promise.resolve(view);
   })
 );
+
+const viewSubmissionPayload: ViewOutput = {
+  "id": "V111",
+  "team_id": "T111",
+  "type": "modal",
+  "blocks": [
+    {
+      "type": "divider",
+      "block_id": "+3ht"
+    }
+  ],
+  "private_metadata": "",
+  "callback_id": "",
+  "state": {
+    "values": {
+      "aPVYH": {
+        "g/t5": {
+          "type": "radio_buttons",
+          "selected_option": null
+        }
+      },
+      "1pSa": {
+        "h3R": {
+          "type": "multi_static_select",
+          "selected_options": []
+        }
+      },
+      "a/Rt": {
+        "zmPQ": {
+          "type": "plain_text_input",
+          "value": null
+        }
+      },
+      "7/wWO": {
+        "HdJj": {
+          "type": "plain_text_input",
+          "value": "test"
+        }
+      }
+    }
+  },
+  "hash": "1618378109.3ndA0Spf",
+  "title": {
+    "type": "plain_text",
+    "text": "Workplace check-in",
+    "emoji": true
+  },
+  "clear_on_close": false,
+  "notify_on_close": false,
+  "close": {
+    "type": "plain_text",
+    "text": "Cancel",
+    "emoji": true
+  },
+  "submit": {
+    "type": "plain_text",
+    "text": "Submit",
+    "emoji": true
+  },
+  "previous_view_id": null,
+  "root_view_id": "V1234567890",
+  "app_id": "A02",
+  "external_id": "",
+  "app_installed_team_id": "T5J4Q04QG",
+  "bot_id": "B00"
+};
+expectType<ViewOutput>(viewSubmissionPayload);
\ No newline at end of file