Skip to content

fix(api): use linked Slack user identity for MCP auth#1447

Merged
saddlepaddle merged 1 commit into
mainfrom
slack-use-auth
Feb 13, 2026
Merged

fix(api): use linked Slack user identity for MCP auth#1447
saddlepaddle merged 1 commit into
mainfrom
slack-use-auth

Conversation

@saddlepaddle
Copy link
Copy Markdown
Collaborator

@saddlepaddle saddlepaddle commented Feb 12, 2026

Summary

  • Fixes Slack impersonation vulnerability: Previously, runSlackAgent always used connectedByUserId (the person who installed the integration) as the identity for MCP tool calls. Any Slack user in the workspace could act as that person. Now each request resolves the actual Superset user via the usersSlackUsers link table.
  • Gates Slack bot on Pro plan: Both process-mention and process-assistant-message check for an active subscription before proceeding. Free orgs see an "Upgrade to Pro" button.
  • Requires account linking: Unlinked Slack users see a "Connect Account" button (Block Kit) with an HMAC-signed connect URL instead of executing tools.
  • Extracts generateConnectUrl to shared utility: Moved from process-app-home-opened to utils/generate-connect-url/ so both entry points can reuse it.
  • Updates billing page: Added deep link ("Open in Desktop App") and download buttons instead of just a text placeholder.

Test plan

  • Unlinked Slack user @mentions the bot → sees "Connect Account" button, no tools executed
  • Free org user @mentions the bot → sees "Upgrade to Pro" button
  • Linked Pro user @mentions the bot → works as before, using their own identity
  • Same checks for DM (assistant message) flow
  • Visit /settings/billing on web → see deep link + download buttons
  • Visit /settings/billing?success=true → see payment success message (unchanged)

Summary by CodeRabbit

  • New Features

    • Updated Billing page: desktop app download, “Open in Desktop App” action, and refreshed payment success UI.
  • Improvements

    • Slack interactions now require an active Pro subscription and prompt upgrades when absent.
    • Slack prompts users to connect their account when not linked and uses stored user preferences for agent interactions.
  • Refactor

    • Connection URL generation and Slack agent invocation simplified and centralized for consistent behavior.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds subscription and account-link gating to Slack event handlers, extracts connect-URL generation to a new utility, passes caller-supplied userId and modelPreference into the Slack agent runtime, removes a DB connection lookup from run-agent, and updates billing page success/layout.

Changes

Cohort / File(s) Summary
Slack Event Handlers - Gating & Agent Params
apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts, apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts
Run concurrent queries for subscriptions and slackUserLink; short-circuit to post Pro-plan or connect-account prompts when missing; use generateConnectUrl for connect links; pass userId and modelPreference into the agent invocation.
Slack App Home Handler
apps/api/src/app/api/integrations/slack/events/process-app-home-opened/process-app-home-opened.ts
Replaced inline HMAC/token logic and removed env/crypto imports; now uses shared generateConnectUrl for connectUrl generation.
URL Generation Utility
apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts, apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/index.ts
Added generateConnectUrl module that builds a time-limited HMAC-SHA256-signed token and returns a connect URL using NEXT_PUBLIC_API_URL; re-exported via index.
Slack Agent Runtime
apps/api/src/app/api/integrations/slack/events/utils/run-agent/run-agent.ts
Removed DB lookup for integration connection and related imports; added userId: string to RunSlackAgentParams; use caller-provided userId for MCP client and agent context.
Billing Page UI
apps/web/src/app/(dashboard)/settings/billing/page.tsx
Inverted success check and revised UI: shows "Payment Successful" for success; otherwise renders billing header and action buttons (Open in Desktop App, Download for Mac) with updated layout and protocol handling.

Sequence Diagram(s)

sequenceDiagram
    participant User as Slack User
    participant Handler as Event Handler
    participant DB as Database
    participant Utils as URL Utility
    participant Agent as Slack Agent
    participant Slack as Slack API

    User->>Handler: send mention / DM / app_home_opened
    Handler->>DB: fetch active subscription & slackUserLink (concurrent)
    DB-->>Handler: return subscription, slackUserLink

    alt No Active Subscription
        Handler->>Slack: post Pro-plan upgrade prompt
        Slack-->>User: show upgrade prompt
    else No Slack Link
        Handler->>Utils: generateConnectUrl(slackUserId, teamId)
        Utils-->>Handler: connectUrl
        Handler->>Slack: post connect-account prompt with connectUrl
        Slack-->>User: show connect prompt
    else Subscription + Link present
        Handler->>Agent: invoke runSlackAgent(params including userId & modelPreference)
        Agent->>Slack: perform actions / post messages
        Slack-->>User: deliver agent response
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through handlers, checked subscription and link,
I signed a short-lived token in just a blink,
I handed my userId to the agent with care,
Prompted upgrades and connects for users out there,
A carrot, a hop—now workflows are in sync 🥕✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and concisely describes the primary change: using linked Slack user identity for MCP authentication instead of relying on the integration installer's identity.
Description check ✅ Passed The PR description provides a comprehensive summary of all changes, includes a detailed test plan with checkboxes, and covers the main objectives including the vulnerability fix, gating logic, URL generation extraction, and billing page updates.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch slack-use-auth

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (1)

53-137: Heavy duplication with process-mention.ts.

The subscription gate (lines 72-99) and account-link gate (lines 101-137) — including the Slack Block Kit payloads — are virtually identical between this file and process-mention.ts. Consider extracting a shared helper (e.g., assertSlackPrerequisites) that performs both checks and returns early with the appropriate Slack message, or returns the resolved slackUserLink. This would reduce the maintenance surface for changes like the "trialing" status fix.

apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts (2)

53-62: event.user guard is unnecessary for AppMentionEvent.

Unlike GenericMessageEvent (where user can be undefined), AppMentionEvent.user is a required string. The ternary on line 54 won't hurt, but it adds dead-code noise. A minor nit — fine to leave if you prefer consistency between the two handlers.


91-91: Hardcoded billing URL appears in both handlers.

"https://app.superset.sh/settings/billing" is repeated across both process-mention.ts and process-assistant-message.ts. Extract it to a shared constant (or derive from env.NEXT_PUBLIC_API_URL) to avoid drift if the domain or path changes. As per coding guidelines, "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic".


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 12, 2026

🧹 Preview Cleanup Complete

The following preview resources have been cleaned up:

  • ✅ Neon database branch
  • ✅ Electric Fly.io app
  • ✅ Streams Fly.io app

Thank you for your contribution! 🎉

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts`:
- Around line 101-106: Add a guard to ensure event.user is defined before
calling generateConnectUrl: if event.user is undefined, return early (same
pattern used in the process-mention flow). Locate the block that checks
slackUserLink and before calling generateConnectUrl({ slackUserId: event.user,
teamId }) add "if (!event.user) return;" so you never pass an undefined
slackUserId to generateConnectUrl or include "undefined" in the signed payload;
keep the existing slack.chat.postMessage usage unchanged.

In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx:
- Around line 36-41: Replace the plain anchor using the custom scheme with logic
that handles the missing desktop app: create a handler (e.g., handleOpenDesktop)
and change the Button/anchor usage (Button asChild + <a href="superset://...">)
to call that handler; in the handler attempt to open the superset:// URL (via
window.location or an iframe) and after a short timeout redirect or show a
prompt to the desktop download page if the scheme wasn't handled; additionally
render a small fallback note component under the Button (e.g., "Don't have the
app? Download it below.") and ensure the ExternalLink icon remains for visual
parity.
🧹 Nitpick comments (4)
apps/web/src/app/(dashboard)/settings/billing/page.tsx (1)

36-47: Download link only targets Mac ARM64; consider broader platform support.

The "Download for Mac" button exclusively links to the ARM64 .dmg. Users on Intel Macs, Windows, or Linux have no download option. If this is intentional for now, a brief comment or a TODO would help clarify intent. If not, consider detecting the platform or linking to a general releases page.

Also, the download <a> should include target="_blank" rel="noopener noreferrer" so the current page isn't navigated away during download, and to follow secure linking best practices.

Suggested fix for the download link
 				<Button variant="outline" size="lg" className="gap-2" asChild>
-					<a href={DOWNLOAD_URL_MAC_ARM64}>
+					<a href={DOWNLOAD_URL_MAC_ARM64} target="_blank" rel="noopener noreferrer">
 						Download for Mac
 						<Download className="size-4" />
 					</a>
 				</Button>
apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts (2)

14-14: Extract the expiry duration to a named constant.

The 10 * 60 * 1000 magic number should be a named constant for clarity and single-point-of-change if the TTL needs adjustment.

Proposed fix
+const CONNECT_URL_TTL_MS = 10 * 60 * 1000; // 10 minutes
+
 export function generateConnectUrl({
 	slackUserId,
 	teamId,
 }: {
 	slackUserId: string;
 	teamId: string;
 }): string {
 	const payload = JSON.stringify({
 		slackUserId,
 		teamId,
-		exp: Date.now() + 10 * 60 * 1000,
+		exp: Date.now() + CONNECT_URL_TTL_MS,
 	});

As per coding guidelines, "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic".


16-18: Both link verification and signature generation are properly aligned and working correctly.

The link verification endpoint (link/route.ts) and the connect URL generation (generate-connect-url.ts) both use SLACK_SIGNING_SECRET with the same HMAC-SHA256 algorithm to sign and validate the token payload. The signatures are correctly matched and validated.

Note: SLACK_SIGNING_SECRET is reused for two distinct purposes—verifying incoming Slack webhooks (verify-signature.ts using the v0:timestamp:body format) and signing internal connect URL tokens (using a plain JSON payload). While these use different payload formats which prevents signature confusion, the coupling of these two security domains to the same key material remains a design choice. As originally suggested, if the application grows significantly, consider dedicating a separate key for internal token signing to reduce blast radius if either security domain is compromised.

apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (1)

72-136: Duplicated Block Kit messages across process-assistant-message and process-mention.

The "Upgrade to Pro" and "Connect Account" Block Kit payloads are nearly identical in both files. Consider extracting them into shared helper functions in the ../utils/slack-blocks module to reduce duplication and ensure consistency.

Comment on lines +36 to +41
<Button size="lg" className="gap-2" asChild>
<a href="superset://settings/billing">
Open in Desktop App
<ExternalLink className="size-4" />
</a>
</Button>
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

Custom URL scheme superset:// will fail ungracefully if the desktop app is not installed.

When the desktop app isn't installed, clicking "Open in Desktop App" will either show a browser error or do nothing depending on the browser. Consider adding a fallback — e.g., a small note below the button ("Don't have the app? Download it below.") or using a mechanism that detects whether the scheme was handled.

🤖 Prompt for AI Agents
In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx around lines 36 - 41,
Replace the plain anchor using the custom scheme with logic that handles the
missing desktop app: create a handler (e.g., handleOpenDesktop) and change the
Button/anchor usage (Button asChild + <a href="superset://...">) to call that
handler; in the handler attempt to open the superset:// URL (via window.location
or an iframe) and after a short timeout redirect or show a prompt to the desktop
download page if the scheme wasn't handled; additionally render a small fallback
note component under the Button (e.g., "Don't have the app? Download it below.")
and ensure the ExternalLink icon remains for visual parity.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx:
- Around line 49-54: The "Download for Mac" Button currently links only to
DOWNLOAD_URL_MAC_ARM64 which serves an Apple Silicon (ARM64) binary; update the
UI to avoid misleading users by either implementing client-side platform
detection (use navigator.userAgent or navigator.platform in the component that
renders Button) to choose between DOWNLOAD_URL_MAC_ARM64 and a
DOWNLOAD_URL_MAC_INTEL (or other platform URLs) and render the appropriate link,
or at minimum change the label text to "Download for Mac (Apple Silicon)" and/or
add a secondary dropdown/menu offering other platform links; locate the Button
usage in page.tsx and the DOWNLOAD_URL_MAC_ARM64 symbol to apply the fix.
🧹 Nitpick comments (3)
apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (2)

72-99: Consider extracting the billing URL to a shared constant.

The URL "https://app.superset.sh/settings/billing" is hardcoded here and likely duplicated in process-mention.ts. Extracting it to a named constant (e.g., in a shared constants file alongside generateConnectUrl) would keep these in sync if the URL ever changes.


101-102: Silent early return when event.user is undefined.

If event.user is undefined and we reach this point (subscription is active, but no link can be resolved), we exit silently. Adding a brief log here would help with debugging unexpected Slack events that lack a user field.

Proposed improvement
 	if (!slackUserLink) {
-		if (!event.user) return;
+		if (!event.user) {
+			console.warn("[slack/process-assistant-message] event.user is undefined, skipping connect prompt");
+			return;
+		}
 		const connectUrl = generateConnectUrl({
apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts (1)

88-93: Hardcoded billing URL — consider using a shared constant or env variable.

Line 91 uses a hardcoded "https://app.superset.sh/settings/billing" while other URL construction in this PR (e.g., generateConnectUrl) uses env.NEXT_PUBLIC_API_URL. If the app domain varies by environment (staging, self-hosted), this URL will point to the wrong place.

Comment on lines +49 to +54
<Button variant="outline" size="lg" className="gap-2" asChild>
<a href={DOWNLOAD_URL_MAC_ARM64}>
Download for Mac
<Download className="size-4" />
</a>
</Button>
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

🧩 Analysis chain

🏁 Script executed:

rg -n 'DOWNLOAD_URL' --type=ts --type=tsx

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

cd apps/web && wc -l src/app/\(dashboard\)/settings/billing/page.tsx

Repository: superset-sh/superset

Length of output: 112


🏁 Script executed:

cd apps/web && sed -n '1,70p' src/app/\(dashboard\)/settings/billing/page.tsx

Repository: superset-sh/superset

Length of output: 1761


🏁 Script executed:

fd -t f constants -x grep -l DOWNLOAD_URL {}

Repository: superset-sh/superset

Length of output: 98


🏁 Script executed:

rg -n 'DOWNLOAD_URL' --type ts

Repository: superset-sh/superset

Length of output: 1765


🏁 Script executed:

cat -n packages/shared/src/constants.ts | head -50

Repository: superset-sh/superset

Length of output: 2103


🏁 Script executed:

cd apps/web && grep -r "DOWNLOAD_URL\|platform\|Windows\|Intel" --type=ts --type=tsx src/ | head -20

Repository: superset-sh/superset

Length of output: 184


🏁 Script executed:

rg 'Superset.*dmg|Superset.*exe|Superset.*appimage|Intel|x64' packages/shared/src/ apps/

Repository: superset-sh/superset

Length of output: 50378


🏁 Script executed:

cat README.md | head -100

Repository: superset-sh/superset

Length of output: 3633


🏁 Script executed:

find . -name "RELEASE*" -o -name "DOWNLOADS*" -o -name ".github" -type d | head -5

Repository: superset-sh/superset

Length of output: 122


"Download for Mac" button only offers Apple Silicon (ARM64) binary — other platforms are excluded.

The button label "Download for Mac" links exclusively to the ARM64 .dmg. Users on Intel Macs will download an incompatible binary, and Windows/Linux users see a Mac-only option with no alternative. Consider either:

  • Detecting the platform (via user-agent or a client-side check) and offering the appropriate binary, or
  • At minimum, labeling the button more precisely (e.g., "Download for Mac (Apple Silicon)") to set correct expectations.
🤖 Prompt for AI Agents
In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx around lines 49 - 54,
The "Download for Mac" Button currently links only to DOWNLOAD_URL_MAC_ARM64
which serves an Apple Silicon (ARM64) binary; update the UI to avoid misleading
users by either implementing client-side platform detection (use
navigator.userAgent or navigator.platform in the component that renders Button)
to choose between DOWNLOAD_URL_MAC_ARM64 and a DOWNLOAD_URL_MAC_INTEL (or other
platform URLs) and render the appropriate link, or at minimum change the label
text to "Download for Mac (Apple Silicon)" and/or add a secondary dropdown/menu
offering other platform links; locate the Button usage in page.tsx and the
DOWNLOAD_URL_MAC_ARM64 symbol to apply the fix.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In
`@apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts`:
- Around line 63-67: The subscription lookup currently only checks
eq(subscriptions.status, "active") which excludes trial users; update the where
clause used in db.query.subscriptions.findFirst (in
process-assistant-message.ts) to accept both "active" and "trialing" statuses by
replacing the single eq check with an or(eq(subscriptions.status, "active"),
eq(subscriptions.status, "trialing")) (and make the same change in the analogous
query in process-mention.ts) so both active and trialing subscriptions are
treated as valid.

In
`@apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts`:
- Around line 1-21: Replace the non-constant-time signature comparison (the
direct string equality check of sig vs expectedSig) with crypto.timingSafeEqual:
import timingSafeEqual from "node:crypto", convert both sig and expectedSig to
Buffers of the same encoding/length, and call timingSafeEqual(Buffer.from(sig),
Buffer.from(expectedSig)) inside a try/catch so mismatched lengths or errors
return a 401 (“Invalid signature”); update the request handler that validates
the query params (the code that reads token, sig and computes expectedSig) to
use timingSafeEqual instead of `sig !== expectedSig` while keeping the existing
expiry check unchanged.

In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx:
- Line 40: The link currently uses PROTOCOL_SCHEMES.PROD unconditionally; change
it to select PROTOCOL_SCHEMES.DEV when process.env.NODE_ENV !== 'production'
(otherwise use PROD). Update the anchor href construction in page.tsx to derive
the scheme from process.env.NODE_ENV and then build the URL with that scheme
(reference PROTOCOL_SCHEMES and the anchor generating code where
`${PROTOCOL_SCHEMES.PROD}://settings/billing` appears).
🧹 Nitpick comments (3)
apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts (2)

14-14: Extract the token TTL to a named constant.

10 * 60 * 1000 is a magic number. A named constant improves readability and makes it easy to adjust in one place.

Suggested change
+const CONNECT_TOKEN_TTL_MS = 10 * 60 * 1000; // 10 minutes
+
 export function generateConnectUrl({
 	slackUserId,
 	teamId,
 ...
 	const payload = JSON.stringify({
 		slackUserId,
 		teamId,
-		exp: Date.now() + 10 * 60 * 1000,
+		exp: Date.now() + CONNECT_TOKEN_TTL_MS,
 	});

As per coding guidelines, "Extract hardcoded magic numbers, strings, and enums to named constants at module top instead of leaving them inline in logic".


16-16: Verify that SLACK_SIGNING_SECRET is appropriate for this use case.

This reuses Slack's signing secret (intended for verifying requests from Slack) as an HMAC key for your own connect-URL tokens. If Slack rotates this secret, your outstanding connect URLs silently break. Consider whether a dedicated secret (e.g., SLACK_CONNECT_SIGNING_SECRET) would be more appropriate to decouple these concerns.

apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (1)

72-99: Consider extracting the subscription/link gating blocks into a shared helper.

The subscription gate (Lines 72–99) and account-link gate (Lines 101–137) are nearly identical to the corresponding blocks in process-mention.ts (Lines 72–137). This is a significant amount of duplicated Slack Block Kit markup and control flow.

A shared utility (e.g., checkSlackPreconditions) could encapsulate the parallel fetch, subscription check, and link check, returning either the resolved slackUserLink or posting the appropriate Slack message and returning null.

Also applies to: 101-137

Comment on lines +63 to +67
db.query.subscriptions.findFirst({
where: and(
eq(subscriptions.referenceId, connection.organizationId),
eq(subscriptions.status, "active"),
),
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
# Check what subscription statuses exist and how trialing is handled elsewhere
rg -n "subscriptions.status" --type ts -C3 -g '!node_modules'
echo "---"
rg -n "trialing\|trial" --type ts -g '!node_modules' -g '!*.test.*' | head -40

Repository: superset-sh/superset

Length of output: 4016


🏁 Script executed:

#!/bin/bash
# Find subscription status enum/schema definition
rg -n "status.*trialing|trialing.*status" --type ts -g '!node_modules' -A2 -B2

echo "---"
# Look for subscription schema definition
rg -n "subscriptions.*=.*pgTable|type.*Subscription" --type ts -g '!node_modules' -A10 | head -80

echo "---"
# Check if there's trial-related business logic
rg -n "plan.*trial|trial.*plan|isTrial" --type ts -g '!node_modules' -g '!*.test.*' | head -20

Repository: superset-sh/superset

Length of output: 2457


🏁 Script executed:

#!/bin/bash
# Find all places where subscription status is set/updated
rg -n "status.*=.*[\"']" --type ts -g '!node_modules' | grep -i "subscription\|status" | head -30

echo "---"
# Look for Stripe subscription status handling (since status likely comes from Stripe)
rg -n "status.*complete|trial|active" --type ts -g '!node_modules' -g '!*.test.*' | grep -i "stripe\|subscription" | head -20

echo "---"
# Check the Slack integration files more closely
cat apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts | head -80

Repository: superset-sh/superset

Length of output: 7742


🏁 Script executed:

#!/bin/bash
# Find Stripe webhook/event handling for subscriptions
rg -n "stripe.*subscription|webhook.*subscription" --type ts -g '!node_modules' -i | head -20

echo "---"
# Find where subscription status is actually SET (not just queried)
rg -n "subscriptions.*update|\.status.*=|status.*pending|status.*active|status.*trialing" --type ts -g '!node_modules' -g '!*.test.*' | head -30

echo "---"
# Check if there's a plan types enum
rg -n "type.*Plan|enum.*Plan|const.*PLAN" --type ts -g '!node_modules' | head -20

Repository: superset-sh/superset

Length of output: 6778


🏁 Script executed:

#!/bin/bash
# Check the subscription schema more closely for trial-related fields
cat packages/db/src/schema/schema.ts | grep -A 30 "export const subscriptions"

echo "---"
# Check if there's any trial field in the subscription table
rg -n "trial|trialEnds\|trial_ends" --type ts -g '!node_modules' packages/db/src/schema/

echo "---"
# Look for plan handling in the subscription context
rg -n "subscription.*plan|plan.*subscription" --type ts -g '!node_modules' -B2 -A2 | head -50

Repository: superset-sh/superset

Length of output: 4609


🏁 Script executed:

#!/bin/bash
# Find Stripe webhook handlers (likely in auth/server.ts based on earlier grep)
rg -n "events.data.object.status\|customer.subscription" --type ts -g '!node_modules' -B5 -A5 | head -80

echo "---"
# Check if trials are mentioned in any business logic or guards
rg -n "trialStart|trialEnd" --type ts -g '!node_modules' -B2 -A2

Repository: superset-sh/superset

Length of output: 550


🏁 Script executed:

#!/bin/bash
# Find Stripe event type handlers - look for checkout.session.completed, invoice.payment_succeeded, etc.
rg -n "checkout\|invoice\|customer\\.subscription" --type ts packages/auth/src/server.ts -A10 -B2 | head -100

echo "---"
# Look for where subscription records are created with status values
rg -n "subscriptions\\.create\|INSERT.*subscriptions\|db\\.insert.*subscriptions" --type ts -g '!node_modules' -B3 -A10 | head -60

Repository: superset-sh/superset

Length of output: 67


🏁 Script executed:

#!/bin/bash
# Search for subscription creation across entire codebase
rg -n "subscriptions.*insert\|insert.*subscriptions" --type ts -g '!node_modules' -B2 -A8

echo "---"
# Look at the full Stripe webhook file to understand event handling
find . -name "*stripe*" -type f -path "*/src/*" -not -path "*/node_modules/*" | grep -E "\.(ts|tsx)$"

Repository: superset-sh/superset

Length of output: 278


🏁 Script executed:

#!/bin/bash
# Read the stripe.ts file to understand subscription handling
wc -l packages/auth/src/stripe.ts
echo "---"
cat packages/auth/src/stripe.ts

Repository: superset-sh/superset

Length of output: 219


🏁 Script executed:

#!/bin/bash
# Get the size of server.ts
wc -l packages/auth/src/server.ts

echo "---"
# Search for Stripe event handler patterns and subscription-related updates
rg -n "export.*function\|async.*function\|stripeSubscription\|subscription.*update\|subscription.*insert" packages/auth/src/server.ts | head -40

Repository: superset-sh/superset

Length of output: 99


🏁 Script executed:

#!/bin/bash
# Look at the structure of packages/auth/src/server.ts
head -100 packages/auth/src/server.ts

echo "---"
# Search for where subscriptions table is used (insert, update, upsert)
rg -n "db.*subscriptions\|subscriptions.*\(" packages/auth/src/server.ts | head -30

Repository: superset-sh/superset

Length of output: 3382


🏁 Script executed:

#!/bin/bash
# Search for the stripe plugin configuration in server.ts
rg -n "stripe\(" packages/auth/src/server.ts -A30 | head -60

echo "---"
# Search for subscription status assignments
rg -n "status.*=\|update.*status" packages/auth/src/server.ts | grep -i subscription

Repository: superset-sh/superset

Length of output: 981


🌐 Web query:

better-auth stripe plugin subscription status values trialing active

💡 Result:

In the Better Auth Stripe plugin, subscription.status is stored as a string and effectively follows Stripe’s subscription status field. Better Auth’s own examples treat a subscription as “currently valid” if status === "active" || status === "trialing" (and it enforces at most one active or trialing subscription per referenceId at a time). [1]

So, besides "trialing" and "active", you should expect the other Stripe status values as well: incomplete, incomplete_expired, past_due, canceled, unpaid, paused. [2]

Practical rule for access checks (per Better Auth docs/examples): grant access when status is active or trialing; treat the rest as not entitled unless you intentionally support grace periods (e.g., past_due). [1][2]


🏁 Script executed:

#!/bin/bash
# Check all Slack integration files for subscription status checks
rg -n "eq\(subscriptions.status" apps/api/src/app/api/integrations/slack -B5 -A5

echo "---"
# Check if there's any trial-aware logic elsewhere
rg -n "trialing\|trial" packages/auth/src/server.ts

Repository: superset-sh/superset

Length of output: 2602


Update subscription status check to include trial subscriptions.

The query currently filters eq(subscriptions.status, "active"), which excludes trial subscriptions (status = "trialing"). According to Better Auth's Stripe plugin behavior, subscriptions can have status "trialing" during the trial period, and access logic should grant access for both "active" and "trialing" statuses.

This affects both Slack integration files:

  • apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts (line 66)
  • apps/api/src/app/api/integrations/slack/events/process-mention/process-mention.ts (line 66)

Update the query to use or(eq(subscriptions.status, "active"), eq(subscriptions.status, "trialing")) or a similar pattern to include both valid statuses.

🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/events/process-assistant-message/process-assistant-message.ts`
around lines 63 - 67, The subscription lookup currently only checks
eq(subscriptions.status, "active") which excludes trial users; update the where
clause used in db.query.subscriptions.findFirst (in
process-assistant-message.ts) to accept both "active" and "trialing" statuses by
replacing the single eq check with an or(eq(subscriptions.status, "active"),
eq(subscriptions.status, "trialing")) (and make the same change in the analogous
query in process-mention.ts) so both active and trialing subscriptions are
treated as valid.

Comment on lines +1 to +21
import { createHmac } from "node:crypto";
import { env } from "@/env";

export function generateConnectUrl({
slackUserId,
teamId,
}: {
slackUserId: string;
teamId: string;
}): string {
const payload = JSON.stringify({
slackUserId,
teamId,
exp: Date.now() + 10 * 60 * 1000,
});
const signature = createHmac("sha256", env.SLACK_SIGNING_SECRET)
.update(payload)
.digest("hex");
const token = Buffer.from(payload).toString("base64url");
return `${env.NEXT_PUBLIC_API_URL}/api/integrations/slack/link?token=${token}&sig=${signature}`;
}
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for the slack/link endpoint handler
fd -t f \( -name "*.ts" -o -name "*.js" \) | xargs grep -l "slack/link" | head -20

Repository: superset-sh/superset

Length of output: 294


🏁 Script executed:

#!/bin/bash
# Search more broadly for slack integration route files
find . -type f \( -name "*.ts" -o -name "*.js" \) -path "*slack*" -path "*link*" 2>/dev/null

Repository: superset-sh/superset

Length of output: 289


🏁 Script executed:

#!/bin/bash
# Search for signature validation and expiry checks in slack-related files
rg -n "timingSafeEqual|timingConstantEqual|exp.*Date|signature.*verify" --type ts -g "*slack*" 2>/dev/null

Repository: superset-sh/superset

Length of output: 46


🏁 Script executed:

cat apps/api/src/app/api/integrations/slack/link/route.ts

Repository: superset-sh/superset

Length of output: 2704


Fix timing attack vulnerability: use crypto.timingSafeEqual for signature comparison.

The endpoint validates both signature and expiry, but the signature comparison at line 27 uses non-constant-time string equality (sig !== expectedSig), which is vulnerable to timing attacks. Replace with:

Fix signature comparison
import { timingSafeEqual } from "node:crypto";

// Instead of:
if (sig !== expectedSig) {

// Use:
try {
  timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig));
} catch {
  return new Response("Invalid signature", { status: 401 });
}

The expiry validation (lines 34–38) is correct.

🤖 Prompt for AI Agents
In
`@apps/api/src/app/api/integrations/slack/events/utils/generate-connect-url/generate-connect-url.ts`
around lines 1 - 21, Replace the non-constant-time signature comparison (the
direct string equality check of sig vs expectedSig) with crypto.timingSafeEqual:
import timingSafeEqual from "node:crypto", convert both sig and expectedSig to
Buffers of the same encoding/length, and call timingSafeEqual(Buffer.from(sig),
Buffer.from(expectedSig)) inside a try/catch so mismatched lengths or errors
return a 401 (“Invalid signature”); update the request handler that validates
the query params (the code that reads token, sig and computes expectedSig) to
use timingSafeEqual instead of `sig !== expectedSig` while keeping the existing
expiry check unchanged.

</div>
<div className="flex flex-wrap justify-center gap-3">
<Button size="lg" className="gap-2" asChild>
<a href={`${PROTOCOL_SCHEMES.PROD}://settings/billing`}>
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 where PROTOCOL_SCHEMES is defined
rg -n 'PROTOCOL_SCHEMES' --type ts --type tsx -B2 -A2 | head -100

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Check the specific file and line mentioned in the review
cat -n apps/web/src/app/\(dashboard\)/settings/billing/page.tsx | sed -n '35,45p'

Repository: superset-sh/superset

Length of output: 548


🏁 Script executed:

#!/bin/bash
# Look for how process.env.NODE_ENV is used for environment-specific logic in the codebase
rg -n 'process\.env\.NODE_ENV' --type ts --type tsx -C2 | head -80

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for other deep link usage patterns with PROTOCOL_SCHEMES
rg -n 'href.*PROTOCOL_SCHEMES' --type ts --type tsx -B1 -A1

Repository: superset-sh/superset

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Search for PROTOCOL_SCHEMES definition without file type restriction
rg -n 'PROTOCOL_SCHEMES\s*=' -A5 | head -50

Repository: superset-sh/superset

Length of output: 374


🏁 Script executed:

#!/bin/bash
# Search for PROTOCOL_SCHEMES usage with context
rg -n 'PROTOCOL_SCHEMES\.' -B1 -A1 | head -100

Repository: superset-sh/superset

Length of output: 618


🏁 Script executed:

#!/bin/bash
# Look for process.env.NODE_ENV patterns in the codebase
rg -n 'process\.env\.NODE_ENV' -B2 -A2 | head -80

Repository: superset-sh/superset

Length of output: 5195


🏁 Script executed:

#!/bin/bash
# Find constants or config files that might define PROTOCOL_SCHEMES
fd -e ts -e js -e tsx -e jsx | xargs grep -l 'PROTOCOL_SCHEMES' | head -20

Repository: superset-sh/superset

Length of output: 188


Use the appropriate protocol scheme based on environment.

The PROTOCOL_SCHEMES.PROD is hardcoded, which will break deep links in development environments where the desktop app registers superset-dev://. Both DEV and PROD variants exist in PROTOCOL_SCHEMES; select the appropriate scheme based on process.env.NODE_ENV.

Proposed fix
+const protocolScheme =
+	process.env.NODE_ENV === "production"
+		? PROTOCOL_SCHEMES.PROD
+		: PROTOCOL_SCHEMES.DEV;
+
 ...
-				<a href={`${PROTOCOL_SCHEMES.PROD}://settings/billing`}>
+				<a href={`${protocolScheme}://settings/billing`}>
🤖 Prompt for AI Agents
In `@apps/web/src/app/`(dashboard)/settings/billing/page.tsx at line 40, The link
currently uses PROTOCOL_SCHEMES.PROD unconditionally; change it to select
PROTOCOL_SCHEMES.DEV when process.env.NODE_ENV !== 'production' (otherwise use
PROD). Update the anchor href construction in page.tsx to derive the scheme from
process.env.NODE_ENV and then build the URL with that scheme (reference
PROTOCOL_SCHEMES and the anchor generating code where
`${PROTOCOL_SCHEMES.PROD}://settings/billing` appears).

…ller

Previously, any Slack user could impersonate the person who installed
the Slack integration because runSlackAgent always used
connectedByUserId. Now each entry point resolves the actual Superset
user via the usersSlackUsers link table and passes their userId to the
agent. Unlinked users see a connect button; free orgs see an upgrade
button. Also updates the billing page with deep link + download CTAs.
@saddlepaddle saddlepaddle merged commit ee2d332 into main Feb 13, 2026
14 of 15 checks passed
@Kitenite Kitenite deleted the slack-use-auth branch February 16, 2026 00:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant