@vercel/slack-bolt 1.4.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,545 +1,506 @@
1
- import './chunk-B5E7QPZP.mjs';
2
- import { ReceiverAuthenticityError, verifySlackRequest, ReceiverMultipleAckError } from '@slack/bolt';
3
- import { ConsoleLogger, LogLevel } from '@slack/logger';
4
- import { InstallProvider } from '@slack/oauth';
5
- import { waitUntil } from '@vercel/functions';
6
- import { IncomingMessage, ServerResponse } from 'http';
7
- import { Socket } from 'net';
8
-
9
- // src/errors.ts
10
- var ERROR_MESSAGES = {
11
- // VercelReceiver errors
12
- SIGNING_SECRET_REQUIRED: "SLACK_SIGNING_SECRET is required for VercelReceiver",
13
- APP_NOT_INITIALIZED: "App not initialized",
14
- OAUTH_NOT_CONFIGURED: "OAuth is not configured. Provide clientId, clientSecret, and stateSecret to enable OAuth.",
15
- REQUEST_TIMEOUT: "Request timeout",
16
- EVENT_NOT_ACKNOWLEDGED: "Event not acknowledged within timeout period",
17
- // Header validation errors
18
- MISSING_REQUIRED_HEADER: (header) => `Missing required header: ${header}`,
19
- // Generic fallback errors
20
- REQUEST_VERIFICATION_FAILED: "Request verification failed",
21
- INTERNAL_SERVER_ERROR: "Internal server error",
22
- INTERNAL_SERVER_ERROR_HANDLER: "Internal Server Error",
23
- ACKNOWLEDGMENT_ERROR: "Error in acknowledgment handler",
24
- CREATE_HANDLER_ERROR: "Error in createHandler:",
25
- // Error type names
26
- TYPES: {
27
- VERCEL_RECEIVER_ERROR: "VercelReceiverError",
28
- SIGNATURE_VERIFICATION_ERROR: "SignatureVerificationError",
29
- REQUEST_PARSING_ERROR: "RequestParsingError",
30
- UNEXPECTED_ERROR: "UnexpectedError",
31
- HANDLER_ERROR: "HandlerError"
32
- }
1
+ import { ReceiverAuthenticityError, ReceiverMultipleAckError, verifySlackRequest } from "@slack/bolt";
2
+ import { ConsoleLogger, LogLevel } from "@slack/logger";
3
+ import { InstallProvider } from "@slack/oauth";
4
+ import { waitUntil } from "@vercel/functions";
5
+ import { IncomingMessage, ServerResponse } from "node:http";
6
+ import { Socket } from "node:net";
7
+ //#region src/errors.ts
8
+ const ERROR_MESSAGES = {
9
+ SIGNING_SECRET_REQUIRED: "SLACK_SIGNING_SECRET is required for VercelReceiver",
10
+ APP_NOT_INITIALIZED: "App not initialized",
11
+ OAUTH_NOT_CONFIGURED: "OAuth is not configured. Provide clientId, clientSecret, and stateSecret to enable OAuth.",
12
+ REQUEST_TIMEOUT: "Request timeout",
13
+ EVENT_NOT_ACKNOWLEDGED: "Event not acknowledged within timeout period",
14
+ MISSING_REQUIRED_HEADER: (header) => `Missing required header: ${header}`,
15
+ REQUEST_VERIFICATION_FAILED: "Request verification failed",
16
+ INTERNAL_SERVER_ERROR: "Internal server error",
17
+ INTERNAL_SERVER_ERROR_HANDLER: "Internal Server Error",
18
+ ACKNOWLEDGMENT_ERROR: "Error in acknowledgment handler",
19
+ CREATE_HANDLER_ERROR: "Error in createHandler:",
20
+ TYPES: {
21
+ VERCEL_RECEIVER_ERROR: "VercelReceiverError",
22
+ SIGNATURE_VERIFICATION_ERROR: "SignatureVerificationError",
23
+ REQUEST_PARSING_ERROR: "RequestParsingError",
24
+ UNEXPECTED_ERROR: "UnexpectedError",
25
+ HANDLER_ERROR: "HandlerError"
26
+ }
33
27
  };
34
28
  var VercelReceiverError = class extends Error {
35
- constructor(message, statusCode = 500) {
36
- super(message);
37
- this.statusCode = statusCode;
38
- this.name = ERROR_MESSAGES.TYPES.VERCEL_RECEIVER_ERROR;
39
- }
29
+ constructor(message, statusCode = 500) {
30
+ super(message);
31
+ this.statusCode = statusCode;
32
+ this.name = ERROR_MESSAGES.TYPES.VERCEL_RECEIVER_ERROR;
33
+ }
40
34
  };
41
35
  var RequestParsingError = class extends VercelReceiverError {
42
- constructor(message = "Failed to parse request") {
43
- super(message, 400);
44
- this.name = ERROR_MESSAGES.TYPES.REQUEST_PARSING_ERROR;
45
- }
36
+ constructor(message = "Failed to parse request") {
37
+ super(message, 400);
38
+ this.name = ERROR_MESSAGES.TYPES.REQUEST_PARSING_ERROR;
39
+ }
46
40
  };
41
+ /**
42
+ * Determines the appropriate HTTP status code for a given error.
43
+ * @param error The error to get status code for
44
+ * @returns HTTP status code
45
+ */
47
46
  function getStatusCode(error) {
48
- if (error instanceof VercelReceiverError) {
49
- return error.statusCode;
50
- }
51
- if (error && typeof error === "object") {
52
- const errorName = error.constructor?.name || error.name;
53
- switch (errorName) {
54
- case "ReceiverAuthenticityError":
55
- return 401;
56
- case "ReceiverMultipleAckError":
57
- return 500;
58
- case "RequestParsingError":
59
- return 400;
60
- case "SignatureVerificationError":
61
- return 400;
62
- default:
63
- return 500;
64
- }
65
- }
66
- return 500;
47
+ if (error instanceof VercelReceiverError) return error.statusCode;
48
+ if (error && typeof error === "object") switch (error.constructor?.name || error.name) {
49
+ case "ReceiverAuthenticityError": return 401;
50
+ case "ReceiverMultipleAckError": return 500;
51
+ case "RequestParsingError": return 400;
52
+ case "SignatureVerificationError": return 400;
53
+ default: return 500;
54
+ }
55
+ return 500;
67
56
  }
57
+ /**
58
+ * Gets the error message for response.
59
+ * @param error The error to get message for
60
+ * @returns Error message string
61
+ */
68
62
  function getErrorMessage(error) {
69
- if (error && typeof error === "object" && "message" in error) {
70
- return String(error.message);
71
- }
72
- return ERROR_MESSAGES.INTERNAL_SERVER_ERROR;
63
+ if (error && typeof error === "object" && "message" in error) return String(error.message);
64
+ return ERROR_MESSAGES.INTERNAL_SERVER_ERROR;
73
65
  }
66
+ /**
67
+ * Gets the error type for response.
68
+ * @param error The error to get type for
69
+ * @returns Error type string
70
+ */
74
71
  function getErrorType(error) {
75
- if (error && typeof error === "object") {
76
- const ctorName = error.constructor?.name;
77
- const nameProp = error.name;
78
- const errorName = ctorName ?? nameProp ?? ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR;
79
- return errorName === "Error" ? ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR : errorName;
80
- }
81
- return ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR;
72
+ if (error && typeof error === "object") {
73
+ const ctorName = error.constructor?.name;
74
+ const nameProp = error.name;
75
+ const errorName = ctorName ?? nameProp ?? ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR;
76
+ return errorName === "Error" ? ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR : errorName;
77
+ }
78
+ return ERROR_MESSAGES.TYPES.UNEXPECTED_ERROR;
82
79
  }
80
+ //#endregion
81
+ //#region src/oauth-adapters.ts
82
+ /**
83
+ * Minimal Web <-> Node HTTP adapters for @slack/oauth InstallProvider.
84
+ *
85
+ * These do NOT implement the full IncomingMessage/ServerResponse contract.
86
+ * Only the methods actually called by InstallProvider are intercepted.
87
+ * Do not reuse for other Node libraries without verifying which methods
88
+ * they call.
89
+ *
90
+ * Targets @slack/oauth\@3.0.5 InstallProvider internals.
91
+ * Do not upgrade without running the integration tests.
92
+ */
93
+ /**
94
+ * Wraps a Web API Request as a minimal Node.js IncomingMessage
95
+ * so it can be passed to @slack/oauth's InstallProvider methods.
96
+ */
83
97
  function toIncomingMessage(req) {
84
- const url = new URL(req.url);
85
- const msg = new IncomingMessage(new Socket());
86
- msg.url = url.pathname + url.search;
87
- msg.method = req.method;
88
- for (const [key, value] of req.headers.entries()) {
89
- msg.headers[key.toLowerCase()] = value;
90
- }
91
- if (req.body) {
92
- req.arrayBuffer().then((buf) => {
93
- msg.push(Buffer.from(buf));
94
- msg.push(null);
95
- }).catch(() => msg.destroy());
96
- } else {
97
- msg.push(null);
98
- }
99
- return msg;
98
+ const url = new URL(req.url);
99
+ const msg = new IncomingMessage(new Socket());
100
+ msg.url = url.pathname + url.search;
101
+ msg.method = req.method;
102
+ for (const [key, value] of req.headers.entries()) msg.headers[key.toLowerCase()] = value;
103
+ if (req.body) req.arrayBuffer().then((buf) => {
104
+ msg.push(Buffer.from(buf));
105
+ msg.push(null);
106
+ }).catch(() => msg.destroy());
107
+ else msg.push(null);
108
+ return msg;
100
109
  }
110
+ /**
111
+ * Creates a fake ServerResponse that captures setHeader/writeHead/end calls
112
+ * and converts them into a Web API Response.
113
+ *
114
+ * InstallProvider uses res.setHeader() for Location, Set-Cookie, Content-Type
115
+ * and res.getHeader() to read back Set-Cookie before appending.
116
+ */
101
117
  function createResponseCapture() {
102
- let statusCode = 200;
103
- const capturedHeaders = {};
104
- const chunks = [];
105
- const res = new ServerResponse(new IncomingMessage(new Socket()));
106
- const originalSetHeader = res.setHeader.bind(res);
107
- res.setHeader = (name, value) => {
108
- const key = name.toLowerCase();
109
- if (Array.isArray(value)) {
110
- capturedHeaders[key] = [...value];
111
- } else {
112
- capturedHeaders[key] = String(value);
113
- }
114
- return originalSetHeader(name, value);
115
- };
116
- const originalGetHeader = res.getHeader.bind(res);
117
- res.getHeader = (name) => {
118
- const val = capturedHeaders[name.toLowerCase()];
119
- if (val !== void 0) return val;
120
- return originalGetHeader(name);
121
- };
122
- const originalWriteHead = res.writeHead.bind(res);
123
- res.writeHead = (code, ...args) => {
124
- statusCode = code;
125
- const headersArg = args.find(
126
- (a) => a !== void 0 && typeof a === "object" && !Array.isArray(a)
127
- );
128
- if (headersArg) {
129
- for (const [key, value] of Object.entries(headersArg)) {
130
- const lower = key.toLowerCase();
131
- capturedHeaders[lower] = value;
132
- }
133
- }
134
- return originalWriteHead(code, ...args);
135
- };
136
- const originalEnd = res.end.bind(res);
137
- res.end = (chunk, ...args) => {
138
- if (chunk !== void 0 && chunk !== null) {
139
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
140
- }
141
- return originalEnd(chunk, ...args);
142
- };
143
- res.toResponse = () => {
144
- const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : null;
145
- const webHeaders = new Headers();
146
- for (const [key, value] of Object.entries(capturedHeaders)) {
147
- if (Array.isArray(value)) {
148
- for (const v of value) webHeaders.append(key, v);
149
- } else {
150
- webHeaders.set(key, value);
151
- }
152
- }
153
- return new Response(body, { status: statusCode, headers: webHeaders });
154
- };
155
- return res;
118
+ let statusCode = 200;
119
+ const capturedHeaders = {};
120
+ const chunks = [];
121
+ const res = new ServerResponse(new IncomingMessage(new Socket()));
122
+ const originalSetHeader = res.setHeader.bind(res);
123
+ res.setHeader = (name, value) => {
124
+ const key = name.toLowerCase();
125
+ if (Array.isArray(value)) capturedHeaders[key] = [...value];
126
+ else capturedHeaders[key] = String(value);
127
+ return originalSetHeader(name, value);
128
+ };
129
+ const originalGetHeader = res.getHeader.bind(res);
130
+ res.getHeader = (name) => {
131
+ const val = capturedHeaders[name.toLowerCase()];
132
+ if (val !== void 0) return val;
133
+ return originalGetHeader(name);
134
+ };
135
+ const originalWriteHead = res.writeHead.bind(res);
136
+ res.writeHead = (code, ...args) => {
137
+ statusCode = code;
138
+ const headersArg = args.find((a) => a !== void 0 && typeof a === "object" && !Array.isArray(a));
139
+ if (headersArg) for (const [key, value] of Object.entries(headersArg)) {
140
+ const lower = key.toLowerCase();
141
+ capturedHeaders[lower] = value;
142
+ }
143
+ return originalWriteHead(code, ...args);
144
+ };
145
+ const originalEnd = res.end.bind(res);
146
+ res.end = (chunk, ...args) => {
147
+ if (chunk !== void 0 && chunk !== null) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
148
+ return originalEnd(chunk, ...args);
149
+ };
150
+ res.toResponse = () => {
151
+ const body = chunks.length > 0 ? Buffer.concat(chunks).toString() : null;
152
+ const webHeaders = new Headers();
153
+ for (const [key, value] of Object.entries(capturedHeaders)) if (Array.isArray(value)) for (const v of value) webHeaders.append(key, v);
154
+ else webHeaders.set(key, value);
155
+ return new Response(body, {
156
+ status: statusCode,
157
+ headers: webHeaders
158
+ });
159
+ };
160
+ return res;
156
161
  }
157
-
158
- // src/index.ts
159
- var LOG_PREFIX = "[@vercel/slack-bolt]";
160
- var ACK_TIMEOUT_MS = 3001;
161
- var SLACK_RETRY_NUM_HEADER = "x-slack-retry-num";
162
- var SLACK_RETRY_REASON_HEADER = "x-slack-retry-reason";
163
- var SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
164
- var SLACK_SIGNATURE_HEADER = "x-slack-signature";
162
+ //#endregion
163
+ //#region src/index.ts
164
+ const LOG_PREFIX = "[@vercel/slack-bolt]";
165
+ const ACK_TIMEOUT_MS = 3001;
166
+ const SLACK_RETRY_NUM_HEADER = "x-slack-retry-num";
167
+ const SLACK_RETRY_REASON_HEADER = "x-slack-retry-reason";
168
+ const SLACK_TIMESTAMP_HEADER = "x-slack-request-timestamp";
169
+ const SLACK_SIGNATURE_HEADER = "x-slack-signature";
170
+ /**
171
+ * A Slack Bolt receiver implementation designed for Vercel's serverless environment.
172
+ * Handles Slack events, interactions, and slash commands with automatic request verification,
173
+ * background processing, and timeout management.
174
+ *
175
+ * @example
176
+ * ```typescript
177
+ * import { App } from '@slack/bolt';
178
+ * import { VercelReceiver, createHandler } from '@vercel/slack-bolt';
179
+ *
180
+ * const receiver = new VercelReceiver();
181
+ *
182
+ * const app = new App({
183
+ * receiver,
184
+ * token: process.env.SLACK_BOT_TOKEN,
185
+ * signingSecret: process.env.SLACK_SIGNING_SECRET,
186
+ * });
187
+ * ```
188
+ *
189
+ */
165
190
  var VercelReceiver = class {
166
- signingSecret;
167
- signatureVerification;
168
- logger;
169
- customPropertiesExtractor;
170
- ackTimeoutMs;
171
- app;
172
- installer;
173
- installUrlOptions;
174
- installCallbackOptions;
175
- installPathOptions;
176
- stateVerification;
177
- /**
178
- * Gets the logger instance used by this receiver.
179
- * @returns The logger instance
180
- */
181
- getLogger() {
182
- return this.logger;
183
- }
184
- /**
185
- * Creates a new VercelReceiver instance.
186
- *
187
- * @param options - Configuration options for the receiver
188
- * @throws {VercelReceiverError} When signing secret is not provided
189
- *
190
- * @example
191
- * ```typescript
192
- * const receiver = new VercelReceiver();
193
- * ```
194
- */
195
- constructor({
196
- signingSecret = process.env.SLACK_SIGNING_SECRET,
197
- signatureVerification = true,
198
- logger,
199
- logLevel = LogLevel.INFO,
200
- customPropertiesExtractor,
201
- ackTimeoutMs = ACK_TIMEOUT_MS,
202
- clientId = process.env.SLACK_CLIENT_ID,
203
- clientSecret = process.env.SLACK_CLIENT_SECRET,
204
- stateSecret = process.env.SLACK_STATE_SECRET,
205
- scopes,
206
- redirectUri,
207
- installationStore,
208
- installerOptions = {}
209
- } = {}) {
210
- if (!signingSecret) {
211
- throw new VercelReceiverError(ERROR_MESSAGES.SIGNING_SECRET_REQUIRED);
212
- }
213
- this.signingSecret = signingSecret;
214
- this.signatureVerification = signatureVerification;
215
- this.logger = this.createScopedLogger(
216
- logger ?? new ConsoleLogger(),
217
- logLevel
218
- );
219
- this.customPropertiesExtractor = customPropertiesExtractor;
220
- this.ackTimeoutMs = ackTimeoutMs;
221
- this.stateVerification = installerOptions.stateVerification;
222
- if (clientId !== void 0 && clientSecret !== void 0 && (installerOptions.stateVerification === false || stateSecret !== void 0 || installerOptions.stateStore !== void 0)) {
223
- this.installer = new InstallProvider({
224
- clientId,
225
- clientSecret,
226
- stateSecret,
227
- installationStore,
228
- logger,
229
- logLevel,
230
- directInstall: installerOptions.directInstall,
231
- stateStore: installerOptions.stateStore,
232
- stateVerification: installerOptions.stateVerification,
233
- legacyStateVerification: installerOptions.legacyStateVerification,
234
- stateCookieName: installerOptions.stateCookieName,
235
- stateCookieExpirationSeconds: installerOptions.stateCookieExpirationSeconds,
236
- renderHtmlForInstallPath: installerOptions.renderHtmlForInstallPath,
237
- authVersion: installerOptions.authVersion ?? "v2",
238
- clientOptions: installerOptions.clientOptions,
239
- authorizationUrl: installerOptions.authorizationUrl
240
- });
241
- this.installUrlOptions = {
242
- scopes: scopes ?? [],
243
- userScopes: installerOptions.userScopes,
244
- metadata: installerOptions.metadata,
245
- redirectUri
246
- };
247
- this.installCallbackOptions = installerOptions.callbackOptions ?? {};
248
- this.installPathOptions = installerOptions.installPathOptions ?? {};
249
- }
250
- this.logger.debug("VercelReceiver initialized");
251
- }
252
- /**
253
- * Initializes the receiver with a Slack Bolt app instance.
254
- * This method is called automatically by the Bolt framework.
255
- *
256
- * @param app - The Slack Bolt app instance
257
- */
258
- init(app) {
259
- this.app = app;
260
- this.logger.debug("App initialized in VercelReceiver");
261
- }
262
- /**
263
- * Starts the receiver and returns a handler function for processing requests.
264
- * This method is called automatically by the Bolt framework.
265
- *
266
- * @returns A handler function that processes incoming Slack requests
267
- */
268
- async start() {
269
- this.logger.debug("VercelReceiver started");
270
- return this.toHandler();
271
- }
272
- /**
273
- * Stops the receiver. This method is called automatically by the Bolt framework.
274
- */
275
- async stop() {
276
- this.logger.debug("VercelReceiver stopped");
277
- }
278
- /**
279
- * Handles requests to the OAuth install path.
280
- * Renders an "Add to Slack" page or redirects directly to Slack's authorize URL.
281
- */
282
- handleInstall = async (req) => {
283
- if (!this.installer) {
284
- throw new VercelReceiverError(ERROR_MESSAGES.OAUTH_NOT_CONFIGURED);
285
- }
286
- const nodeReq = toIncomingMessage(req);
287
- const capture = createResponseCapture();
288
- await this.installer.handleInstallPath(
289
- nodeReq,
290
- capture,
291
- this.installPathOptions,
292
- this.installUrlOptions
293
- );
294
- return capture.toResponse();
295
- };
296
- /**
297
- * Handles the OAuth redirect callback from Slack.
298
- * Exchanges the authorization code for tokens and stores the installation.
299
- */
300
- handleCallback = async (req) => {
301
- if (!this.installer) {
302
- throw new VercelReceiverError(ERROR_MESSAGES.OAUTH_NOT_CONFIGURED);
303
- }
304
- const nodeReq = toIncomingMessage(req);
305
- const capture = createResponseCapture();
306
- if (this.stateVerification === false) {
307
- await this.installer.handleCallback(
308
- nodeReq,
309
- capture,
310
- this.installCallbackOptions,
311
- this.installUrlOptions
312
- );
313
- } else {
314
- await this.installer.handleCallback(
315
- nodeReq,
316
- capture,
317
- this.installCallbackOptions
318
- );
319
- }
320
- return capture.toResponse();
321
- };
322
- /**
323
- * Creates a handler function that processes incoming Slack requests.
324
- * This is the main entry point for handling Slack events in Vercel.
325
- * It is called automatically by the Bolt framework in the start() method.
326
- *
327
- * @returns A handler function compatible with Vercel's function signature
328
- */
329
- toHandler() {
330
- return async (req) => {
331
- try {
332
- const rawBody = await req.text();
333
- if (this.signatureVerification) {
334
- this.verifyRequest(req, rawBody);
335
- }
336
- const body = await this.parseRequestBody(req, rawBody);
337
- if (body.type === "url_verification") {
338
- this.logger.debug("Handling URL verification challenge");
339
- return Response.json({ challenge: body.challenge });
340
- }
341
- return await this.handleSlackEvent(req, body);
342
- } catch (error) {
343
- return this.handleError(error);
344
- }
345
- };
346
- }
347
- async parseRequestBody(req, rawBody) {
348
- const contentType = req.headers.get("content-type");
349
- try {
350
- if (contentType === "application/x-www-form-urlencoded") {
351
- const parsedBody = {};
352
- const params = new URLSearchParams(rawBody);
353
- for (const [key, value] of params.entries()) {
354
- parsedBody[key] = value;
355
- }
356
- if (typeof parsedBody.payload === "string") {
357
- return JSON.parse(parsedBody.payload);
358
- }
359
- return parsedBody;
360
- }
361
- if (contentType === "application/json") {
362
- return JSON.parse(rawBody);
363
- }
364
- this.logger.warn(`Unexpected content-type detected: ${contentType}`);
365
- return JSON.parse(rawBody);
366
- } catch (e) {
367
- throw new RequestParsingError(
368
- `Failed to parse body as JSON data for content-type: ${contentType}. Error: ${e instanceof Error ? e.message : String(e)}`
369
- );
370
- }
371
- }
372
- async handleSlackEvent(req, body) {
373
- if (!this.app) {
374
- throw new VercelReceiverError(ERROR_MESSAGES.APP_NOT_INITIALIZED, 500);
375
- }
376
- let isAcknowledged = false;
377
- let responseResolver;
378
- let responseRejecter;
379
- const responsePromise = new Promise((resolve, reject) => {
380
- responseResolver = resolve;
381
- responseRejecter = reject;
382
- });
383
- const timeoutId = setTimeout(() => {
384
- if (!isAcknowledged) {
385
- isAcknowledged = true;
386
- this.logger.error(ERROR_MESSAGES.EVENT_NOT_ACKNOWLEDGED);
387
- const error = new VercelReceiverError(
388
- ERROR_MESSAGES.REQUEST_TIMEOUT,
389
- 408
390
- );
391
- responseRejecter(error);
392
- }
393
- }, this.ackTimeoutMs);
394
- const ackFn = async (responseBody) => {
395
- this.logger.debug(`ack() call begins (body: ${responseBody})`);
396
- if (isAcknowledged) {
397
- throw new ReceiverMultipleAckError();
398
- }
399
- isAcknowledged = true;
400
- clearTimeout(timeoutId);
401
- try {
402
- let body2;
403
- if (typeof responseBody === "undefined") {
404
- body2 = void 0;
405
- } else if (typeof responseBody === "string") {
406
- body2 = responseBody;
407
- } else {
408
- body2 = JSON.stringify(responseBody);
409
- }
410
- const response = new Response(body2, {
411
- status: 200,
412
- headers: {
413
- "Content-Type": "application/json"
414
- }
415
- });
416
- responseResolver(response);
417
- } catch (error) {
418
- this.logger.error(ERROR_MESSAGES.ACKNOWLEDGMENT_ERROR, error);
419
- responseRejecter(
420
- error instanceof Error ? error : new Error(String(error))
421
- );
422
- }
423
- };
424
- const event = this.createSlackReceiverEvent({
425
- body,
426
- headers: req.headers,
427
- ack: ackFn,
428
- request: req
429
- });
430
- waitUntil(
431
- this.app.processEvent(event).catch((error) => {
432
- return this.handleError(error);
433
- })
434
- );
435
- try {
436
- return await responsePromise;
437
- } catch (error) {
438
- return this.handleError(error);
439
- }
440
- }
441
- verifyRequest(req, body) {
442
- const timestamp = req.headers.get(SLACK_TIMESTAMP_HEADER);
443
- const signature = req.headers.get(SLACK_SIGNATURE_HEADER);
444
- if (!signature) {
445
- throw new ReceiverAuthenticityError(
446
- ERROR_MESSAGES.MISSING_REQUIRED_HEADER(SLACK_SIGNATURE_HEADER)
447
- );
448
- }
449
- if (!timestamp) {
450
- throw new ReceiverAuthenticityError(
451
- ERROR_MESSAGES.MISSING_REQUIRED_HEADER(SLACK_TIMESTAMP_HEADER)
452
- );
453
- }
454
- try {
455
- verifySlackRequest({
456
- signingSecret: this.signingSecret,
457
- body,
458
- headers: {
459
- "x-slack-signature": signature,
460
- "x-slack-request-timestamp": Number.parseInt(timestamp, 10)
461
- },
462
- logger: this.logger
463
- });
464
- } catch (error) {
465
- throw new ReceiverAuthenticityError(
466
- error instanceof Error ? error.message : "Failed to verify request signature"
467
- );
468
- }
469
- }
470
- createSlackReceiverEvent({
471
- body,
472
- headers,
473
- ack,
474
- request
475
- }) {
476
- const customProperties = this.customPropertiesExtractor ? this.customPropertiesExtractor(request) : {};
477
- const retryNum = headers.get(SLACK_RETRY_NUM_HEADER) || "0";
478
- const retryReason = headers.get(SLACK_RETRY_REASON_HEADER) || "";
479
- return {
480
- body,
481
- ack,
482
- retryNum: Number(retryNum),
483
- retryReason,
484
- customProperties
485
- };
486
- }
487
- handleError(error) {
488
- const errorMessage = getErrorMessage(error);
489
- const errorType = getErrorType(error);
490
- const errorStatusCode = getStatusCode(error);
491
- this.logger.error(error);
492
- return new Response(
493
- JSON.stringify({
494
- error: errorMessage,
495
- type: errorType
496
- }),
497
- {
498
- status: errorStatusCode,
499
- headers: { "content-type": "application/json" }
500
- }
501
- );
502
- }
503
- createScopedLogger(logger, logLevel) {
504
- logger.setLevel(logLevel);
505
- return {
506
- ...logger,
507
- error: (...args) => logger.error?.(LOG_PREFIX, ...args),
508
- warn: (...args) => logger.warn?.(LOG_PREFIX, ...args),
509
- info: (...args) => logger.info?.(LOG_PREFIX, ...args),
510
- debug: (...args) => logger.debug?.(LOG_PREFIX, ...args),
511
- setLevel: logger.setLevel,
512
- getLevel: logger.getLevel
513
- };
514
- }
191
+ signingSecret;
192
+ signatureVerification;
193
+ logger;
194
+ customPropertiesExtractor;
195
+ ackTimeoutMs;
196
+ app;
197
+ installer;
198
+ installUrlOptions;
199
+ installCallbackOptions;
200
+ installPathOptions;
201
+ stateVerification;
202
+ /**
203
+ * Gets the logger instance used by this receiver.
204
+ * @returns The logger instance
205
+ */
206
+ getLogger() {
207
+ return this.logger;
208
+ }
209
+ /**
210
+ * Creates a new VercelReceiver instance.
211
+ *
212
+ * @param options - Configuration options for the receiver
213
+ * @throws {VercelReceiverError} When signing secret is not provided
214
+ *
215
+ * @example
216
+ * ```typescript
217
+ * const receiver = new VercelReceiver();
218
+ * ```
219
+ */
220
+ constructor({ signingSecret = process.env.SLACK_SIGNING_SECRET, signatureVerification = true, logger, logLevel = LogLevel.INFO, customPropertiesExtractor, ackTimeoutMs = ACK_TIMEOUT_MS, clientId = process.env.SLACK_CLIENT_ID, clientSecret = process.env.SLACK_CLIENT_SECRET, stateSecret = process.env.SLACK_STATE_SECRET, scopes, redirectUri, installationStore, installerOptions = {} } = {}) {
221
+ if (!signingSecret) throw new VercelReceiverError(ERROR_MESSAGES.SIGNING_SECRET_REQUIRED);
222
+ this.signingSecret = signingSecret;
223
+ this.signatureVerification = signatureVerification;
224
+ this.logger = this.createScopedLogger(logger ?? new ConsoleLogger(), logLevel);
225
+ this.customPropertiesExtractor = customPropertiesExtractor;
226
+ this.ackTimeoutMs = ackTimeoutMs;
227
+ this.stateVerification = installerOptions.stateVerification;
228
+ if (clientId !== void 0 && clientSecret !== void 0 && (installerOptions.stateVerification === false || stateSecret !== void 0 || installerOptions.stateStore !== void 0)) {
229
+ this.installer = new InstallProvider({
230
+ clientId,
231
+ clientSecret,
232
+ stateSecret,
233
+ installationStore,
234
+ logger,
235
+ logLevel,
236
+ directInstall: installerOptions.directInstall,
237
+ stateStore: installerOptions.stateStore,
238
+ stateVerification: installerOptions.stateVerification,
239
+ legacyStateVerification: installerOptions.legacyStateVerification,
240
+ stateCookieName: installerOptions.stateCookieName,
241
+ stateCookieExpirationSeconds: installerOptions.stateCookieExpirationSeconds,
242
+ renderHtmlForInstallPath: installerOptions.renderHtmlForInstallPath,
243
+ authVersion: installerOptions.authVersion ?? "v2",
244
+ clientOptions: installerOptions.clientOptions,
245
+ authorizationUrl: installerOptions.authorizationUrl
246
+ });
247
+ this.installUrlOptions = {
248
+ scopes: scopes ?? [],
249
+ userScopes: installerOptions.userScopes,
250
+ metadata: installerOptions.metadata,
251
+ redirectUri
252
+ };
253
+ this.installCallbackOptions = installerOptions.callbackOptions ?? {};
254
+ this.installPathOptions = installerOptions.installPathOptions ?? {};
255
+ }
256
+ this.logger.debug("VercelReceiver initialized");
257
+ }
258
+ /**
259
+ * Initializes the receiver with a Slack Bolt app instance.
260
+ * This method is called automatically by the Bolt framework.
261
+ *
262
+ * @param app - The Slack Bolt app instance
263
+ */
264
+ init(app) {
265
+ this.app = app;
266
+ this.logger.debug("App initialized in VercelReceiver");
267
+ }
268
+ /**
269
+ * Starts the receiver and returns a handler function for processing requests.
270
+ * This method is called automatically by the Bolt framework.
271
+ *
272
+ * @returns A handler function that processes incoming Slack requests
273
+ */
274
+ async start() {
275
+ this.logger.debug("VercelReceiver started");
276
+ return this.toHandler();
277
+ }
278
+ /**
279
+ * Stops the receiver. This method is called automatically by the Bolt framework.
280
+ */
281
+ async stop() {
282
+ this.logger.debug("VercelReceiver stopped");
283
+ }
284
+ /**
285
+ * Handles requests to the OAuth install path.
286
+ * Renders an "Add to Slack" page or redirects directly to Slack's authorize URL.
287
+ */
288
+ handleInstall = async (req) => {
289
+ if (!this.installer) throw new VercelReceiverError(ERROR_MESSAGES.OAUTH_NOT_CONFIGURED);
290
+ const nodeReq = toIncomingMessage(req);
291
+ const capture = createResponseCapture();
292
+ await this.installer.handleInstallPath(nodeReq, capture, this.installPathOptions, this.installUrlOptions);
293
+ return capture.toResponse();
294
+ };
295
+ /**
296
+ * Handles the OAuth redirect callback from Slack.
297
+ * Exchanges the authorization code for tokens and stores the installation.
298
+ */
299
+ handleCallback = async (req) => {
300
+ if (!this.installer) throw new VercelReceiverError(ERROR_MESSAGES.OAUTH_NOT_CONFIGURED);
301
+ const nodeReq = toIncomingMessage(req);
302
+ const capture = createResponseCapture();
303
+ if (this.stateVerification === false) await this.installer.handleCallback(nodeReq, capture, this.installCallbackOptions, this.installUrlOptions);
304
+ else await this.installer.handleCallback(nodeReq, capture, this.installCallbackOptions);
305
+ return capture.toResponse();
306
+ };
307
+ /**
308
+ * Creates a handler function that processes incoming Slack requests.
309
+ * This is the main entry point for handling Slack events in Vercel.
310
+ * It is called automatically by the Bolt framework in the start() method.
311
+ *
312
+ * @returns A handler function compatible with Vercel's function signature
313
+ */
314
+ toHandler() {
315
+ return async (req) => {
316
+ try {
317
+ const rawBody = await req.text();
318
+ if (this.signatureVerification) this.verifyRequest(req, rawBody);
319
+ const body = await this.parseRequestBody(req, rawBody);
320
+ if (body.type === "url_verification") {
321
+ this.logger.debug("Handling URL verification challenge");
322
+ return Response.json({ challenge: body.challenge });
323
+ }
324
+ return await this.handleSlackEvent(req, body);
325
+ } catch (error) {
326
+ return this.handleError(error);
327
+ }
328
+ };
329
+ }
330
+ async parseRequestBody(req, rawBody) {
331
+ const contentType = req.headers.get("content-type");
332
+ try {
333
+ if (contentType === "application/x-www-form-urlencoded") {
334
+ const parsedBody = {};
335
+ const params = new URLSearchParams(rawBody);
336
+ for (const [key, value] of params.entries()) parsedBody[key] = value;
337
+ if (typeof parsedBody.payload === "string") return JSON.parse(parsedBody.payload);
338
+ return parsedBody;
339
+ }
340
+ if (contentType === "application/json") return JSON.parse(rawBody);
341
+ this.logger.warn(`Unexpected content-type detected: ${contentType}`);
342
+ return JSON.parse(rawBody);
343
+ } catch (e) {
344
+ throw new RequestParsingError(`Failed to parse body as JSON data for content-type: ${contentType}. Error: ${e instanceof Error ? e.message : String(e)}`);
345
+ }
346
+ }
347
+ async handleSlackEvent(req, body) {
348
+ if (!this.app) throw new VercelReceiverError(ERROR_MESSAGES.APP_NOT_INITIALIZED, 500);
349
+ let isAcknowledged = false;
350
+ let responseResolver;
351
+ let responseRejecter;
352
+ const responsePromise = new Promise((resolve, reject) => {
353
+ responseResolver = resolve;
354
+ responseRejecter = reject;
355
+ });
356
+ const timeoutId = setTimeout(() => {
357
+ if (!isAcknowledged) {
358
+ isAcknowledged = true;
359
+ this.logger.error(ERROR_MESSAGES.EVENT_NOT_ACKNOWLEDGED);
360
+ const error = new VercelReceiverError(ERROR_MESSAGES.REQUEST_TIMEOUT, 408);
361
+ responseRejecter(error);
362
+ }
363
+ }, this.ackTimeoutMs);
364
+ const ackFn = async (responseBody) => {
365
+ this.logger.debug(`ack() call begins (body: ${responseBody})`);
366
+ if (isAcknowledged) throw new ReceiverMultipleAckError();
367
+ isAcknowledged = true;
368
+ clearTimeout(timeoutId);
369
+ try {
370
+ let body;
371
+ if (typeof responseBody === "undefined") body = void 0;
372
+ else if (typeof responseBody === "string") body = responseBody;
373
+ else body = JSON.stringify(responseBody);
374
+ const response = new Response(body, {
375
+ status: 200,
376
+ headers: { "Content-Type": "application/json" }
377
+ });
378
+ responseResolver(response);
379
+ } catch (error) {
380
+ this.logger.error(ERROR_MESSAGES.ACKNOWLEDGMENT_ERROR, error);
381
+ responseRejecter(error instanceof Error ? error : new Error(String(error)));
382
+ }
383
+ };
384
+ const event = this.createSlackReceiverEvent({
385
+ body,
386
+ headers: req.headers,
387
+ ack: ackFn,
388
+ request: req
389
+ });
390
+ waitUntil(this.app.processEvent(event).catch((error) => {
391
+ return this.handleError(error);
392
+ }));
393
+ try {
394
+ return await responsePromise;
395
+ } catch (error) {
396
+ return this.handleError(error);
397
+ }
398
+ }
399
+ verifyRequest(req, body) {
400
+ const timestamp = req.headers.get(SLACK_TIMESTAMP_HEADER);
401
+ const signature = req.headers.get(SLACK_SIGNATURE_HEADER);
402
+ if (!signature) throw new ReceiverAuthenticityError(ERROR_MESSAGES.MISSING_REQUIRED_HEADER(SLACK_SIGNATURE_HEADER));
403
+ if (!timestamp) throw new ReceiverAuthenticityError(ERROR_MESSAGES.MISSING_REQUIRED_HEADER(SLACK_TIMESTAMP_HEADER));
404
+ try {
405
+ verifySlackRequest({
406
+ signingSecret: this.signingSecret,
407
+ body,
408
+ headers: {
409
+ "x-slack-signature": signature,
410
+ "x-slack-request-timestamp": Number.parseInt(timestamp, 10)
411
+ },
412
+ logger: this.logger
413
+ });
414
+ } catch (error) {
415
+ throw new ReceiverAuthenticityError(error instanceof Error ? error.message : "Failed to verify request signature");
416
+ }
417
+ }
418
+ createSlackReceiverEvent({ body, headers, ack, request }) {
419
+ const customProperties = this.customPropertiesExtractor ? this.customPropertiesExtractor(request) : {};
420
+ const retryNum = headers.get(SLACK_RETRY_NUM_HEADER) || "0";
421
+ const retryReason = headers.get(SLACK_RETRY_REASON_HEADER) || "";
422
+ return {
423
+ body,
424
+ ack,
425
+ retryNum: Number(retryNum),
426
+ retryReason,
427
+ customProperties
428
+ };
429
+ }
430
+ handleError(error) {
431
+ const errorMessage = getErrorMessage(error);
432
+ const errorType = getErrorType(error);
433
+ const errorStatusCode = getStatusCode(error);
434
+ this.logger.error(error);
435
+ return new Response(JSON.stringify({
436
+ error: errorMessage,
437
+ type: errorType
438
+ }), {
439
+ status: errorStatusCode,
440
+ headers: { "content-type": "application/json" }
441
+ });
442
+ }
443
+ createScopedLogger(logger, logLevel) {
444
+ logger.setLevel(logLevel);
445
+ return {
446
+ ...logger,
447
+ error: (...args) => logger.error?.(LOG_PREFIX, ...args),
448
+ warn: (...args) => logger.warn?.(LOG_PREFIX, ...args),
449
+ info: (...args) => logger.info?.(LOG_PREFIX, ...args),
450
+ debug: (...args) => logger.debug?.(LOG_PREFIX, ...args),
451
+ setLevel: logger.setLevel,
452
+ getLevel: logger.getLevel
453
+ };
454
+ }
515
455
  };
456
+ /**
457
+ * Creates a Vercel-compatible handler function for a Slack Bolt app.
458
+ * This is the recommended way to create handlers for deployment on Vercel.
459
+ *
460
+ * @param {App} app - The initialized Slack Bolt app instance.
461
+ * @param {VercelReceiver} receiver - The VercelReceiver instance.
462
+ * @returns {VercelHandler} A handler function compatible with Vercel's function signature.
463
+ *
464
+ * @example
465
+ * ```typescript
466
+ * // api/events.ts
467
+ * import { createHandler } from '@vercel/slack-bolt';
468
+ * import { app, receiver } from '../app';
469
+ *
470
+ * const handler = createHandler(app, receiver);
471
+ *
472
+ * export const POST = async (req: Request) => {
473
+ * return handler(req);
474
+ * };
475
+ * ```
476
+ *
477
+ * @throws {Error} If app initialization fails.
478
+ * @throws {VercelReceiverError} If request processing fails.
479
+ */
516
480
  function createHandler(app, receiver) {
517
- let initPromise = null;
518
- return async (req) => {
519
- try {
520
- if (!initPromise) {
521
- initPromise = app.init().catch((error) => {
522
- initPromise = null;
523
- throw error;
524
- });
525
- }
526
- await initPromise;
527
- receiver.init(app);
528
- const handler = await receiver.start();
529
- return handler(req);
530
- } catch (error) {
531
- console.error(ERROR_MESSAGES.CREATE_HANDLER_ERROR, error);
532
- return new Response(
533
- JSON.stringify({
534
- error: ERROR_MESSAGES.INTERNAL_SERVER_ERROR_HANDLER,
535
- type: ERROR_MESSAGES.TYPES.HANDLER_ERROR
536
- }),
537
- { status: 500, headers: { "content-type": "application/json" } }
538
- );
539
- }
540
- };
481
+ let initPromise = null;
482
+ return async (req) => {
483
+ try {
484
+ if (!initPromise) initPromise = app.init().catch((error) => {
485
+ initPromise = null;
486
+ throw error;
487
+ });
488
+ await initPromise;
489
+ receiver.init(app);
490
+ return (await receiver.start())(req);
491
+ } catch (error) {
492
+ console.error(ERROR_MESSAGES.CREATE_HANDLER_ERROR, error);
493
+ return new Response(JSON.stringify({
494
+ error: ERROR_MESSAGES.INTERNAL_SERVER_ERROR_HANDLER,
495
+ type: ERROR_MESSAGES.TYPES.HANDLER_ERROR
496
+ }), {
497
+ status: 500,
498
+ headers: { "content-type": "application/json" }
499
+ });
500
+ }
501
+ };
541
502
  }
542
-
503
+ //#endregion
543
504
  export { VercelReceiver, createHandler };
544
- //# sourceMappingURL=index.mjs.map
505
+
545
506
  //# sourceMappingURL=index.mjs.map