Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { GenericMessageEvent } from "@slack/types";
import { db } from "@superset/db/client";
import { integrationConnections } from "@superset/db/schema";
import { and, eq } from "drizzle-orm";
import { formatErrorForSlack, runSlackAgent } from "../utils/run-agent";
import {
formatErrorForSlack,
resolveUserMentions,
runSlackAgent,
} from "../utils/run-agent";
import { formatSideEffectsMessage } from "../utils/slack-blocks";
import { createSlackClient } from "../utils/slack-client";

Expand Down Expand Up @@ -60,8 +64,13 @@ export async function processAssistantMessage({
}

try {
const resolve = await resolveUserMentions({
texts: [event.text ?? ""],
slack,
});

const result = await runSlackAgent({
prompt: event.text ?? "",
prompt: resolve(event.text ?? ""),
channelId: event.channel,
threadTs,
organizationId: connection.organizationId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Receiver } from "@upstash/qstash";
import { z } from "zod";

import { env } from "@/env";
import { processAssistantMessage } from "../../events/process-assistant-message";

const receiver = new Receiver({
currentSigningKey: env.QSTASH_CURRENT_SIGNING_KEY,
nextSigningKey: env.QSTASH_NEXT_SIGNING_KEY,
});

const payloadSchema = z.object({
event: z.object({
type: z.literal("message"),
user: z.string(),
text: z.string().optional(),
ts: z.string(),
channel: z.string(),
channel_type: z.literal("im"),
event_ts: z.string(),
thread_ts: z.string().optional(),
}),
teamId: z.string(),
eventId: z.string(),
});

export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("upstash-signature");

if (!signature) {
return Response.json({ error: "Missing signature" }, { status: 401 });
}

const isValid = await receiver.verify({
body,
signature,
url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
});

if (!isValid) {
return Response.json({ error: "Invalid signature" }, { status: 401 });
}
Comment on lines +35 to +43
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@upstash/qstash Receiver verify method throws invalid signature documentation

💡 Result:

The Upstash QStash docs indicate Receiver.verify(...) will fail (often surfacing as “Invalid signature”) when any input used for verification doesn’t exactly match what QStash signed, especially:

  • You didn’t use the raw request body: many frameworks parse JSON into an object; re-stringifying it (e.g. JSON.stringify(req.body)) can change spacing/key order and cause verification to fail. The docs explicitly warn to use the raw body string “as is.” [1]
  • The url you pass doesn’t exactly match the JWT sub claim (scheme/host/path). The manual verification example shows the verifier checks p.sub !== url and throws (subject mismatch). So passing only a host, missing https://, missing path, or using a different public URL than QStash is calling will fail verification. [2]
  • Wrong signing keys (or keys were rolled and your app didn’t update). QStash uses “current” and “next” signing keys; your receiver should verify with both. [1]

Helpful cross-check: QStash delivers the body unchanged (“passed as is”), so your handler must read it in that same raw form for hashing/verification. [3]

Sources: Upstash “Verify Signatures” doc (raw body + Receiver usage) [1], Upstash AWS Lambda quickstart (shows sub/url and body-hash checks) [2], Upstash “Receiving Messages” (body passed as-is) [3].


🌐 Web query:

@upstash/qstash Receiver.verify() return type throws error documentation examples

💡 Result:

In @upstash/qstash (TypeScript), Receiver.verify(...) is async and effectively works like:

  • Return type: Promise<boolean> (it resolves to true when verification succeeds).
  • Failure mode: it throws a SignatureError (it does not return false for invalid signatures). The SDK throws on JWT verification errors, subject (url) mismatch, or body-hash mismatch. [1]

So usage patterns in docs/examples differ, but both are valid:

Pattern A: try/catch (recommended, since failures throw)

try {
  await receiver.verify({ signature, body, url });
  // verified
} catch (err) {
  // invalid signature (SignatureError)
}

Example shown in Upstash’s AWS Lambda quickstart. [2]

Pattern B: assign to isValid (works, but false is not expected)

const isValid = await receiver.verify({ signature, body, url });
if (!isValid) {
  // typically unreachable; invalid cases throw instead
}

Shown in Upstash docs and quickstarts. [3]

Docs/examples: Receiver verify examples are in the “Verify Signatures” doc and the Receiver SDK example page. [3]

Sources:
[1] UNPKG compiled source for @upstash/qstash showing SignatureError and verify() implementation/behavior.
[2] Upstash AWS Lambda (Node) quickstart demonstrating try/catch around verify().
[3] Upstash docs pages showing const isValid = await receiver.verify(...) usage.


receiver.verify() throws on invalid signatures — the isValid check is unreachable.

The QStash Receiver.verify() method throws a SignatureError when the signature is invalid rather than returning false. This means the if (!isValid) block is dead code, and an invalid signature will produce an unhandled exception (500 error) instead of the intended 401 response. Wrap the verify call in a try-catch to properly handle verification failures.

Proposed fix
-	const isValid = await receiver.verify({
-		body,
-		signature,
-		url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
-	});
-
-	if (!isValid) {
-		return Response.json({ error: "Invalid signature" }, { status: 401 });
-	}
+	try {
+		await receiver.verify({
+			body,
+			signature,
+			url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
+		});
+	} catch (err) {
+		console.error("[slack/process-assistant-message] Invalid QStash signature:", err);
+		return Response.json({ error: "Invalid signature" }, { status: 401 });
+	}
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
around lines 35 - 43, receiver.verify throws a SignatureError on invalid
signatures so the existing isValid check is unreachable; wrap the call to
receiver.verify(...) in a try/catch inside route.ts (the handler around
receiver.verify), catch SignatureError (or any verification error) and return
Response.json({ error: "Invalid signature" }, { status: 401 }) from the catch
block to prevent a 500, otherwise proceed when verification succeeds.


const parsed = payloadSchema.safeParse(JSON.parse(body));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

JSON.parse(body) can throw on malformed input, resulting in an unhandled 500.

If QStash delivers a corrupted or non-JSON body that passes signature verification, JSON.parse will throw before the Zod validation can catch the issue. Wrap it or use a safe parse.

🛡️ Proposed fix — combine with the Zod parse in a single try-catch
-	const parsed = payloadSchema.safeParse(JSON.parse(body));
-	if (!parsed.success) {
+	let jsonBody: unknown;
+	try {
+		jsonBody = JSON.parse(body);
+	} catch {
+		return Response.json({ error: "Invalid JSON" }, { status: 400 });
+	}
+
+	const parsed = payloadSchema.safeParse(jsonBody);
+	if (!parsed.success) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const parsed = payloadSchema.safeParse(JSON.parse(body));
let jsonBody: unknown;
try {
jsonBody = JSON.parse(body);
} catch {
return Response.json({ error: "Invalid JSON" }, { status: 400 });
}
const parsed = payloadSchema.safeParse(jsonBody);
if (!parsed.success) {
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
at line 45, The code calls payloadSchema.safeParse(JSON.parse(body)) which lets
JSON.parse throw on malformed input; wrap the JSON.parse in a try-catch (or use
a safe JSON parse helper) before passing the result to payloadSchema.safeParse
and handle the parse error by returning a 400/invalid-body response and logging
the error; update the code around payloadSchema.safeParse, JSON.parse and body
in route.ts to first attempt a safe parse, then call
payloadSchema.safeParse(parsedBody).

if (!parsed.success) {
console.error(
"[slack/process-assistant-message] Invalid payload:",
parsed.error,
);
return Response.json({ error: "Invalid payload" }, { status: 400 });
}

await processAssistantMessage({
event: { ...parsed.data.event, subtype: undefined },
teamId: parsed.data.teamId,
eventId: parsed.data.eventId,
});

return Response.json({ success: true });
}
Comment on lines +27 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find all route.ts files under jobs directories to identify QStash job handlers
echo "=== QStash Job Routes ==="
find . -path "*/jobs/*/route.ts" -type f | head -20

echo -e "\n=== Content of similar job routes ==="
find . -path "*/jobs/*/route.ts" -type f -exec sh -c 'echo "File: $1"; head -80 "$1"; echo "---"' _ {} \;

Repository: superset-sh/superset

Length of output: 11813


🏁 Script executed:

#!/bin/bash
# Search for receiver.verify patterns to see how signature verification is done elsewhere
echo "=== Receiver.verify usages ==="
rg -n 'receiver\.verify' --type ts --type tsx -B2 -A5

Repository: superset-sh/superset

Length of output: 139


🏁 Script executed:

#!/bin/bash
# Also search for other patterns of Upstash/QStash integration patterns
echo "=== QStash/Upstash patterns ==="
rg -n 'upstash|QStash|Receiver' --type ts --type tsx -l | head -20

Repository: superset-sh/superset

Length of output: 140


Add development environment check to skip signature verification in development.

This route lacks the development environment check present in other QStash job handlers (github/initial-sync, linear/initial-sync). In development, QStash cannot reach localhost, making signature verification impossible. Wrap the receiver.verify() call in an isDev check:

const isDev = env.NODE_ENV === "development";

if (!isDev) {
  if (!signature) {
    return Response.json({ error: "Missing signature" }, { status: 401 });
  }

  const isValid = await receiver.verify({
    body,
    signature,
    url: `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/jobs/process-assistant-message`,
  });

  if (!isValid) {
    return Response.json({ error: "Invalid signature" }, { status: 401 });
  }
}
🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/jobs/process-assistant-message/route.ts`
around lines 27 - 61, The POST handler should skip QStash signature verification
in development: introduce isDev (e.g., const isDev = env.NODE_ENV ===
"development") at the top of POST and wrap the existing signature checks and the
receiver.verify(...) call (and its signature presence check) in if (!isDev) {
... } so that in development the route proceeds without requiring
upstash-signature or calling receiver.verify, while keeping the payloadSchema
parsing and processAssistantMessage(...) call unchanged.