Skip to content

Commit a77ec51

Browse files
committed
execute fn in a subprocess like local-run.ts
1 parent 015aa23 commit a77ec51

4 files changed

Lines changed: 196 additions & 58 deletions

File tree

src/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,18 @@ This library has four modes of operation:
2525
3. Using `self-hosted-socket-mode.ts` as the entrypoint. This establishes a persistent
2626
WebSocket connection to Slack using Socket Mode and maintains a long-running process that listens for events.
2727
4. Using `self-hosted-http-mode.ts` as the entrypoint. This starts an HTTP server that
28-
accepts events via POST requests to `/events` endpoints, similar to
29-
Bolt JS's HTTPReceiver.
28+
accepts events via POST requests to `/events` endpoints, similar to Bolt JS's
29+
HTTPReceiver. The only required environment variable is
30+
`SLACK_SIGNING_SECRET`; optional variables include `PORT`,
31+
`SLACK_SIGNATURE_VERIFICATION`, and `SLACK_API_URL` (e.g.
32+
`SLACK_SIGNING_SECRET=xxx PORT=3000 deno run src/self-hosted-http-mode.ts`).
3033

3134
Regardless of which mode of operation used, each runtime definition for a
3235
function is specified in its own file and must be the default export.
3336

3437
## Usage
3538

36-
By default, your Slack app has a `/slack.json` file that defines a `get-hooks`
39+
By default, your Slack app has a `.slack/hooks.json` file that defines a `get-hooks`
3740
hook. The Slack CLI will automatically use the version of the
3841
`deno-slack-runtime` that is specified by the version of the `get-hooks` script
3942
that you're using. To use this library via the Slack CLI out of the box, use the
@@ -47,7 +50,7 @@ You also have the option to
4750
You can change the script that runs by specifying a new script for the `start`
4851
command. For instance, if you wanted to point to your local instance of this
4952
repo, you could accomplish that by adding a `start` command to your
50-
`/slack.json` file and setting it to the following:
53+
`.slack/hooks.json` file and setting it to the following:
5154

5255
```json
5356
{
@@ -66,9 +69,9 @@ operating this library in:
6669
2. Local project with a manifest file:
6770
`deno run -q --config=deno.jsonc --allow-read --allow-net https://deno.land/x/deno_slack_runtime@0.1.1/local-run.ts`
6871
3. Self-hosted socket mode:
69-
`deno run -q --config=deno.jsonc --allow-read --allow-net --allow-env https://deno.land/x/deno_slack_runtime@0.1.1/self-hosted-socket-mode.ts`
72+
`deno run -q --config=deno.jsonc --allow-read --allow-net --allow-run --allow-env --allow-sys=osRelease https://deno.land/x/deno_slack_runtime@0.1.1/self-hosted-socket-mode.ts`
7073
4. Self-hosted HTTP Receiver:
71-
`deno run -q --config=deno.jsonc --allow-read --allow-net --allow-env https://deno.land/x/deno_slack_runtime@0.1.1/self-hosted-http-mode.ts`
74+
`deno run -q --config=deno.jsonc --allow-read --allow-net --allow-env --allow-sys=osRelease https://deno.land/x/deno_slack_runtime@0.1.1/self-hosted-http-mode.ts`
7275

7376
⚠️ Don't forget to update the version specifier in the URL inside the above
7477
commands to match the version you want to test! You can also drop the `@` and

src/local-run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export const getCommandline = function (
5656
"--config=deno.jsonc",
5757
"--allow-read",
5858
"--allow-env",
59+
"--allow-sys=osRelease",
5960
];
6061

6162
const allowedDomains = manifest.outgoing_domains ?? [];

src/self-hosted-http-mode.ts

Lines changed: 102 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)