Skip to content

fix(slack): judge a Slack bind by its secret reference, not an object that is always there - #1875

Merged
lilyshen0722 merged 1 commit into
mainfrom
fix/slack-authorize-empty-pendingbind
Sep 25, 2026
Merged

lilyshen0722 merged 1 commit into
mainfrom
fix/slack-authorize-empty-pendingbind

Conversation

@lilyshen0722

Copy link
Copy Markdown
Contributor

No one can connect Slack on commonly.me, and no one has been able to since #1537 (09-04). Found in the stranger walk for the readiness matrix on build 9a32fca5: Add → Connect (install 200) → Authorize in Slack → POST /api/installables/slack/authorize-url → 409 slack_already_authorized, "Slack is already awaiting confirmation or connected." It happens on a fresh install whose stored row has no chatId and no pendingBind.

Cause

config.pendingBind is a nested schema path (models/Integration.ts), so a hydrated document carries it as {} even when the stored row has none. The route read it by truthiness (installables.ts:169), so every row looked already bound. I proved it in the running backend pod with the compiled model on the stranger's row, which gives doc.config.pendingBind → {}, truthy.

The confirm and reject routes had the same read inverted: if (!pending) could never fire, so slack_bind_missing was unreachable and they went on to a CAS keyed on an undefined botTokenRef.

Fix

All three sites now ask hasPendingBind(config), meaning whether the callback stored a bind, judged by the botTokenRef it always writes. The $exists filters in the CAS updates read the stored row and were already right, so they are unchanged.

Why the suite missed it, and the test

slackOAuth.installables.test.js mocks Integration with plain objects, where an absent key really is absent. The new block hands the routes a real Integration.hydrate(...) document, the shape findOne returns in production:

  • a guard that the hydrated shape still carries a truthy pendingBind (if a schema change ends that, the tests below stop proving anything)
  • a fresh install gets an authorize URL: red on main (409)
  • a row that holds a bind is still refused as already authorized
  • confirm / reject with no bind are named slack_bind_missing and write nothing: red on main

Mutation-checked: with routes/installables.ts restored from origin/main, 3 of the 5 new tests fail and the other 11 pass. With the fix, 14/14 pass. eslint routes/installables.ts is clean.

Not in this PR

After the 409 the page names nothing: the Slack row stays on "Authorize in Slack" at 1200 and 390, with no error rendered. The two screenshots under docs/design/evidence/slack-authorize-409/ are that "before" pair. The page fix is a separate frontend row in the Connectors lane (Kai, after Row A).

Gate: Vera.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RUTWqLu6zhFPraoPHZcjQP

… that is always there

Every new Slack install on commonly.me has been refused at "Authorize in
Slack" since #1537: POST /api/installables/slack/authorize-url answers 409
slack_already_authorized on a row that holds no bind. `config.pendingBind`
is a nested schema path, so the hydrated document carries it as `{}` even
when the stored row has none, and the route read it by truthiness. The
confirm and reject routes had the same read inverted, so `slack_bind_missing`
could never fire and they went on to a CAS keyed on an undefined botTokenRef.

All three now ask whether the callback stored a bind, by the botTokenRef it
always writes. The $exists filters in the CAS updates read the stored row
and were already right.

The existing suite could not see this: it mocks Integration with plain
objects, where an absent key is absent. The new tests hand the routes a real
`Integration.hydrate(...)` document, the shape findOne returns, and three of
them fail on main.

The two screenshots are the stranger walk on 9a32fca at 1200 and 390 after
the 409: the page names nothing. That is a separate frontend row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUTWqLu6zhFPraoPHZcjQP
@lilyshen0722
lilyshen0722 added this pull request to the merge queue Sep 25, 2026
Merged via the queue into main with commit f536fa1 Sep 25, 2026
14 checks passed
lilyshen0722 added a commit to thantiklermcirony/commonly that referenced this pull request Oct 9, 2026
…rule the Slack defect earned (Team-Commonly#1877)

* docs: the 09-25 stranger walk of GitHub, Slack and Telegram, and the rule the Slack defect earned

The readiness matrix gains the walk's per-cell results and corrects two
rows it had wrong: Slack was recorded as stable while every new install was
refused at Authorize in Slack (fixed in Team-Commonly#1875), and GitHub was recorded as
disabled while it has been live on our own repository since 09-18.

Review-checklist rule 34 is rule 24's inverse: a hand-built fixture also
lies about absence, because a nested schema path hydrates as {} on every
document. Checked on mongoose 7.8.6: schema.path() is undefined for the
nested path, schema.nested[] is true, the hydrated value is {} and truthy,
and toJSON drops it, which is why no API response ever showed it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUTWqLu6zhFPraoPHZcjQP

* docs(plans): GitHub C9 expiry is green and C4 lapsed unverified

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RUTWqLu6zhFPraoPHZcjQP

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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