@@ -22,6 +22,20 @@ export interface HTTPReceiverOptions {
2222 slackApiUrl ?: string ;
2323}
2424
25+ /**
26+ * Prompts the user for the Request URL to set under settings.event_subscriptions
27+ * in the app manifest. Call this before apps.manifest.validate so the user can set
28+ * the URL in the manifest before validation runs.
29+ * @returns The trimmed URL string, or null if the user skipped (Enter with no input).
30+ */
31+ export function promptRequestUrlForEventSubscriptions ( ) : string | null {
32+ const raw = prompt (
33+ "Request URL to set under settings.event_subscriptions in your app manifest (optional; press Enter to skip):" ,
34+ ) ;
35+ const url = raw ?. trim ( ) ?? null ;
36+ return url === "" ? null : url ;
37+ }
38+
2539/**
2640 * Constant-time string comparison to mitigate timing attacks (aligned with bolt-js verify-request).
2741 */
@@ -93,6 +107,23 @@ async function verifySlackRequest(
93107 return timingSafeEqual ( computedHash , signatureHash ) ;
94108}
95109
110+ /**
111+ * Parse event endpoint body like bolt-js HTTPModuleFunctions.parseHTTPRequestBody:
112+ * application/x-www-form-urlencoded with a "payload" field, or raw JSON.
113+ */
114+ // deno-lint-ignore no-explicit-any
115+ function parseEventRequestBody ( bodyText : string , contentType : string | null ) : any {
116+ if ( contentType ?. includes ( "application/x-www-form-urlencoded" ) ) {
117+ const params = new URLSearchParams ( bodyText ) ;
118+ const payload = params . get ( "payload" ) ;
119+ if ( typeof payload === "string" ) {
120+ return JSON . parse ( payload ) ;
121+ }
122+ return Object . fromEntries ( params ) ;
123+ }
124+ return JSON . parse ( bodyText ) ;
125+ }
126+
96127/**
97128 * Runs the app as an HTTP server that accepts Slack events via POST to the
98129 * configured endpoints. Request handling (verify → parse → ssl_check →
@@ -148,11 +179,55 @@ export const runWithHTTPReceiver = async function (
148179
149180 // Health check endpoint
150181 if ( request . method === "GET" && pathname === "/health" ) {
182+ // TODO: remove
183+ hookCLI . log ( `[info] ${ request . method } ${ pathname } called` ) ;
151184 return new Response ( "OK" , { status : 200 } ) ;
152185 }
153186
187+ // POST /functions — raw invocation payload (no signature verification), same as mod.ts
188+ if ( request . method === "POST" && pathname === "/functions" ) {
189+ // TODO: remove
190+ hookCLI . log ( `[info] ${ request . method } ${ pathname } called` ) ;
191+ try {
192+ const body = await request . text ( ) ;
193+ // deno-lint-ignore no-explicit-any
194+ const payload : InvocationPayload < any > = JSON . parse ( body ) ;
195+ const resp = await dispatch ( payload , hookCLI , ( functionCallbackId ) => {
196+ const functionDefn = manifest . functions [ functionCallbackId ] ;
197+ if ( ! functionDefn ) {
198+ throw new Error (
199+ `No function definition for function callback id ${ functionCallbackId } was found in the manifest! manifest.functions: ${
200+ Object . keys ( manifest . functions ) . join ( ", " )
201+ } `,
202+ ) ;
203+ }
204+ const functionFile =
205+ `file://${ workingDirectory } /${ functionDefn . source_file } ` ;
206+ return functionFile ;
207+ } ) ;
208+ return new Response ( JSON . stringify ( resp ?? { } ) , {
209+ status : 200 ,
210+ headers : { "Content-Type" : "application/json" } ,
211+ } ) ;
212+ } catch ( error ) {
213+ hookCLI . error ( "❌ Error processing /functions request:" , error ) ;
214+ if ( error instanceof Error && error . stack ) {
215+ hookCLI . error ( error . stack ) ;
216+ }
217+ return new Response (
218+ JSON . stringify ( { error : String ( error ) } ) ,
219+ {
220+ status : 500 ,
221+ headers : { "Content-Type" : "application/json" } ,
222+ } ,
223+ ) ;
224+ }
225+ }
226+
154227 // Event endpoints
155228 if ( request . method === "POST" && eventEndpoints . includes ( pathname ) ) {
229+ // TODO: remove
230+ hookCLI . log ( `[info] ${ request . method } ${ pathname } called` ) ;
156231 try {
157232 const body = await request . text ( ) ;
158233
@@ -169,26 +244,27 @@ export const runWithHTTPReceiver = async function (
169244 }
170245 }
171246
172- // Parse the body
173- // deno-lint-ignore no-explicit-any
174- const parsedBody : any = JSON . parse ( body ) ;
247+ // Parse the body (JSON or form-encoded with payload= like bolt-js)
248+ const parsedBody = parseEventRequestBody (
249+ body ,
250+ request . headers . get ( "content-type" ) ,
251+ ) ;
175252
176253 // Log incoming event
177254 const eventType = parsedBody . type || parsedBody . event ?. type ||
178255 "unknown" ;
179256 const eventId = parsedBody . event_id || "no-id" ;
180257 hookCLI . log ( `📨 Received event: ${ eventType } (${ eventId } )` ) ;
181258
182- // Handle URL verification challenge
259+ // Handle URL verification challenge (response format matches bolt-js
260+ // HTTPReceiver buildUrlVerificationResponse)
183261 if ( parsedBody . type === "url_verification" ) {
184262 hookCLI . log ( "✅ Responding to URL verification challenge" ) ;
185- return new Response (
186- JSON . stringify ( { challenge : parsedBody . challenge } ) ,
187- {
188- status : 200 ,
189- headers : { "Content-Type" : "application/json" } ,
190- } ,
191- ) ;
263+ const challenge = parsedBody . challenge ;
264+ return new Response ( JSON . stringify ( { challenge } ) , {
265+ status : 200 ,
266+ headers : { "Content-Type" : "application/json" } ,
267+ } ) ;
192268 }
193269
194270 // Handle SSL check
@@ -281,12 +357,15 @@ export const runWithHTTPReceiver = async function (
281357 }
282358
283359 // 404 for unknown routes
360+ // TODO: remove
361+ hookCLI . log ( `[info] ${ request . method } ${ pathname } — not found (404)` ) ;
284362 return new Response ( "Not Found" , { status : 404 } ) ;
285363 } ;
286364
287365 // Start the server
288366 hookCLI . log ( `🚀 Starting HTTP server on port ${ port } ` ) ;
289367 hookCLI . log ( `📡 Event endpoints: ${ eventEndpoints . join ( ", " ) } ` ) ;
368+ hookCLI . log ( `📮 Invocation endpoint: POST /functions` ) ;
290369 hookCLI . log ( `🏥 Health check: /health` ) ;
291370
292371 const server = Deno . serve ( {
@@ -345,6 +424,18 @@ if (import.meta.main) {
345424
346425 const hookCLI = getProtocolInterface ( Deno . args ) ;
347426
427+ // Prompt for the Request URL before any manifest validate; caller can set it in manifest then validate
428+ const requestUrl = promptRequestUrlForEventSubscriptions ( ) ;
429+ if ( requestUrl ) {
430+ console . log (
431+ `Set this Request URL in your app manifest under /settings/event_subscriptions: ${ requestUrl } ` ,
432+ ) ;
433+ } else {
434+ console . log (
435+ "Remember to set the Request URL under settings.event_subscriptions in your app manifest when using Event Subscriptions." ,
436+ ) ;
437+ }
438+
348439 await runWithHTTPReceiver (
349440 getManifest ,
350441 DispatchPayload ,
0 commit comments