Repository: slackapi/bolt-js (ts)

Issue:
ack(options) does not compile in TypeScript
### Description

Why do I have this error?
Do you have a solution to specify the right type?

I want to send options in a select menu in modal

#### Attachments:

![image](https://user-images.githubusercontent.com/19187357/103173198-7ceb0b80-4859-11eb-8687-0d1c82e4fb30.png)


Pull request:
Fix #720 ack(options) does not compile in TypeScript
###  Summary

This pull request resolves #720 

As most developers (perhaps 100%) handle only block_suggestion requests in `app.options` listeners. Thus, this pull request set the default Source to `block_suggestion` to ensure better developer experience in TypeScript.

If a developer would like to handle either `interactive_message` (user actions on attachments) or `dialog_suggestion` (the ones in legacy dialogs), the developer can have an explicit type parameter as below:

```typescript
app.options<'interactive_message'>({ callback_id: 'a' }, async ({ ack }) => {
  await ack();
});

app.options<'dialog_suggestion'>({ callback_id: 'a' }, async ({ ack }) => {
  await ack();
});
```

### 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) {


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