Skip to content

fix(slack): robust workspace setup (resumable wizard, user-token OAuth, scope parsing) - #106

Open
DouwMarx wants to merge 1 commit into
mruwnik:masterfrom
DouwMarx:slack-setup-robustness
Open

fix(slack): robust workspace setup (resumable wizard, user-token OAuth, scope parsing)#106
DouwMarx wants to merge 1 commit into
mruwnik:masterfrom
DouwMarx:slack-setup-robustness

Conversation

@DouwMarx

@DouwMarx DouwMarx commented Jul 9, 2026

Copy link
Copy Markdown

Found doing a full Slack workspace setup end to end against a fresh instance. Two genuine bugs blocked/degraded setup, plus a wizard dead-end.

Bugs

  1. Bot scope in the OAuth authorize URL blocks install. The URL sent both a bot scope and user_scope, but the backend only stores and uses the user token (_persist_oauth_credentials reads authed_user; there is no bot-token column). Slack then requires the app to have a bot user and fails install with "doesn't have a bot user to install". Fixed by requesting user_scope only.

  2. Granted scopes parsed on whitespace, not comma. Slack returns scope comma-separated ("channels:history,users:read"). .split() collapsed it into one bogus element, so any per-scope membership check ("channels:history" in creds.scopes) was always false. Fixed with .split(",").

Robustness

  1. Resumable setup wizard. A half-configured draft app was a dead end: reopening the wizard looped back through already-completed OAuth with no path to the remaining steps. Now it starts at the first unfinished step (from the client_secret / authorized / signing_secret flags) and re-fetches the wizard nonce on resume; the Sources panel surfaces a "Finish setup" entry for apps not yet live.

Scopes are unchanged from upstream and stay read-only (user-token ingest, never posts).

Docs / tests

  • README: a short Slack setup runbook with the gotchas above.
  • Tests: authorize URL carries user_scope and no bot scope; granted scopes parse on comma.

All frontend and backend Slack tests pass.

🤖 Generated with Claude Code

…h, scope parsing)

Discovered doing a full Slack setup end to end against a fresh instance:

- OAuth requests user_scope only. It previously also sent a bot `scope`, but
  the backend uses only the user token (the exchange discards the bot token and
  there is no bot-token column), so requesting bot scope made Slack demand a
  bot user the app never uses and failed install with "doesn't have a bot user
  to install".
- The setup wizard is now resumable: it starts at the first unfinished step
  (from the client_secret/authorized/signing_secret flags, not setup_state
  alone) and re-fetches the wizard nonce on resume; the Sources panel surfaces
  a "Finish setup" entry for apps not yet live. This fixes the draft-app dead
  end where a half-configured app looped back through completed OAuth.
- Parse Slack's comma-separated granted scopes with split(",") instead of a
  whitespace split that collapsed them into one bogus element (so any
  per-scope membership check was always false).
- Scopes stay read-only: the integration ingests and never posts to Slack.
- README: add a Slack setup runbook and the gotchas.
- Tests: guard that the authorize URL carries user_scope and no bot scope, and
  that granted scopes parse on comma.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@mruwnik mruwnik left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ame-no-Uzume (Claude Opus 4.8): ## Review — fix(slack): robust workspace setup

Solid, well-scoped PR. The three fixes are individually correct and the comments/tests are unusually good (they explain the why, not just the what). No blocking issues; my findings are reconciliation and robustness polish.

Verified correct:

  • user_scope-only OAuth — matches the backend, which reads only authed_user and has no bot-token column. Test asserting user_scope carries scopes and scope is absent is exactly right.
  • Comma-split of granted scopes (slack.py:436) — [s for s in ....split(",") if s] correctly handles Slack's comma-separated scope and filters empties. Correct fix.
  • resumeStep — keying resume off the client_secret_configured / is_authorized / signing_secret_configured flags rather than setup_state is the right call; the draft-with-OAuth-already-done loop it fixes is real. Step ordering is sound, and all three flags exist on SlackAppResponse.

Findings (all non-blocking):

  1. (README + wizard, medium) README step 6 correctly says subscribe "on behalf of users" with four message.* events, but the wizard's events-url step (unchanged) still says "Subscribe to bot events" and lists extra reaction_*/channel_* events. For a no-bot-user app these contradict each other and the new user-token model — reconcile the wizard text with the README. Details inline on README:204.
  2. (SlackAppWizard.tsx:185, low) On resume, the nonce fetch is the only one, and its failure is swallowed with console.warn → blank wizard body with no error/retry. Surface it via setError to match the non-resume path. On-theme with the PR's dead-end fix.
  3. (SlackPanel.tsx:175, low) "Finish setup" banner isn't filtered by is_owner — confirm listApps is owner-scoped server-side, else a non-owner gets a button that fails mid-wizard. Plus the banner's inline yellow-* utilities are light-mode-only.

One neutral note on severity: I couldn't find any current consumer that does a per-scope membership check against SlackUserCredentials.scopes (grepped src/), so the comma-split fix is latent-correctness hygiene rather than an active bug fix today — the PR body's "always false" framing slightly overstates present impact. The fix is still correct and worth keeping.

I'll keep watching and re-review as the PR evolves.

Comment thread README.md
3. Under OAuth & Permissions, add User Token Scopes: `channels:history`, `groups:history`, `im:history`, `mpim:history`, `channels:read`, `groups:read`, `im:read`, `mpim:read`, `users:read`, `users:read.email`, `team:read`, `reactions:read`, `files:read`. The wizard requests exactly this set.
4. Authorize the workspace.
5. Paste the Signing Secret into the wizard.
6. Under Event Subscriptions, set the Request URL (verified via the signing secret). Then under "Subscribe to events on behalf of users" add `message.channels`, `message.groups`, `message.im`, `message.mpim`. URL verification alone does not deliver messages; the event subscription is a separate step.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ame-no-Uzume (Claude Opus 4.8): The README here correctly says to subscribe "on behalf of users" with only the four message.* events — consistent with the user-token-only model this PR establishes. But the wizard UI (SlackAppWizard.tsx, the events-url step, unchanged by this PR) still tells the user the opposite:

Subscribe to bot events:
  • message.channels / groups / im / mpim
  • reaction_added / reaction_removed
  • channel_created / channel_renamed / channel_archived

Two concerns:

  1. "bot events" vs "on behalf of users". Since the whole point of fix Fix lint errors and add CI workflow #1 is that the app has no bot user ("doesn't have a bot user to install"), Slack's "Subscribe to bot events" section isn't the right one — it generally requires a bot user. The README's "on behalf of users" is correct; the wizard should match, or a user following the on-screen wizard will land in the section that reproduces the bot-user problem. (~60% confident this is an actual setup snag; you completed setup end-to-end, so maybe Slack tolerates it or you added a bot user manually — worth confirming.)

  2. Event set mismatch. The wizard lists reaction_* and channel_* events; the README lists only the four message.*. If reaction/channel ingestion actually needs those subscriptions, the README under-documents them; if it doesn't, the wizard over-asks. Either way the two should agree.

Reconciling the wizard text with this README section would close the loop on the user-token migration.

.then(n => {
if (!cancelled) setNonceData(n)
})
.catch(e => console.warn('Wizard nonce fetch failed:', e))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ame-no-Uzume (Claude Opus 4.8): On the resume path this is the only place the nonce gets fetched, and a failure is swallowed with console.warn. The oauth and events-url steps render nothing until callbackUrl/eventsUrl exist ({step === 'oauth' && app && callbackUrl && ...}), so if issueWizardNonce fails here the user is left staring at a wizard with a header, a progress bar, and an empty body — no error, no retry.

That's exactly the dead-end class this PR is trying to eliminate. The non-resume path surfaces this failure (handleClientSecret calls handleError); the resume path should too — setError((e as Error).message) in the catch, so at least the user sees why the step is blank and can reopen. Minor, but cheap and on-theme.

</button>
</div>

{unfinishedApps.length > 0 && !wizardOpen && (

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Ame-no-Uzume (Claude Opus 4.8): Two things on the "Finish setup" banner:

  1. Ownership. unfinishedApps is listApps() filtered only by setup_state, not by is_owner. If GET /slack/apps returns apps the current user can see but doesn't own (multi-tenant), a non-owner gets a "Finish setup" button that walks them into setClientSecret / setSigningSecret, which should fail server-side with a permission error. If listApps is already owner-scoped on the backend this is moot — worth a quick confirm. If not, filter on a.is_owner here (the field exists on SlackAppResponse).

  2. Styling (nit). This banner uses raw border-yellow-300 bg-yellow-50 text-yellow-900 utilities inline while the rest of the panel goes through the styles module. Those are light-mode-only colors — if the app has a dark theme, yellow-900 text on yellow-50 won't adapt. Low priority, but it stands out against the surrounding convention.

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.

2 participants