requestApproval: a click from a non-approver locks the real approver out of Approve
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 76/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- node.js, typescript
Research direction
Start by reading packages/chat/src/callback-url.ts around resolveCallbackUrl and packages/chat/src/workflow/approval.test.ts around the existing unauthorized-click test. Reproduce the sequence in the issue: a non-approver clicks Approve, then the approver clicks the same button. Done means the approver's click reaches the workflow and approves, while unauthorized users still cannot approve.
Written by the indexing model from the issue text.
Description
Bug description
If someone outside approvers clicks Approve on a requestApproval card, the real approver can't approve anymore. Their Approve click does nothing. They can still click Deny, or wait for the timeout.
Non-approvers still can't approve, but one stray click from them is enough to block the approval.
Steps to reproduce
- Call
requestApproval(thread, { title: "Deploy?", approvers: ["U_ALICE"] }). U_MALLORYclicks Approve. She gets the "isn't authorized" notice, as expected.U_ALICEclicks Approve.
Expected behavior
Step 3 approves.
Actual behavior
Alice's click never reaches the workflow webhook, so the workflow keeps waiting. Deny still works because it has its own token.
Cause
The card's Approve and Deny buttons are callbackUrl buttons (approval-card.ts#L63-L69). Each button's token is single use: resolveCallbackUrl deletes it on the first click from anyone (callback-url.ts#L177). The approvers check only runs later, inside requestApproval, after that click has already been POSTed to the webhook (workflow/index.ts#L174). By the time Alice clicks, the Approve token is gone, resolveCallbackUrl returns null, and nothing is forwarded (chat.ts#L1930).
The existing test "ignores clicks from users outside approvers and posts a notice" (approval.test.ts#L234) misses this for two reasons. It emits clicks straight to the webhook, so resolveCallbackUrl never runs, and the approver clicks Deny rather than the button Mallory used.
One possible fix: keep approval tokens alive until finalizeApprovalCard runs, or check the approver list before a token is consumed.
Code sample
I reproduced it with the real Slack adapter against a local fake of the Slack API, sending signed block_actions payloads. To isolate the token step, I used two plain callbackUrl buttons, the same buttons requestApproval builds, instead of running inside a workflow runtime:
// chat 4.43.0, @chat-adapter/slack 4.43.0, @chat-adapter/state-memory 4.43.0
const bot = new Chat({
userName: "bot",
adapters: { slack: createSlackAdapter({ botToken, signingSecret, apiUrl: fakeSlackUrl }) },
state: createMemoryState(),
});
bot.onNewMention(async (thread) => {
await thread.post(
Card({
title: "Deploy?",
children: [
Actions([
Button({ id: "approve", label: "Approve", callbackUrl }),
Button({ id: "deny", label: "Deny", callbackUrl }),
]),
],
})
);
});
// Signed block_actions webhooks for the posted card, one after another:
await click("U_MALLORY", approveButton); // POSTed to callbackUrl (1 POST)
await click("U_ALICE", approveButton); // not POSTed (still 1)
await click("U_ALICE", denyButton); // POSTed (2)
Environment
Chat SDK 4.43.0 (main at 7c351d2 has the same code), Node.js 24.21.0, Slack adapter, Linux.
- Dominant language
- TypeScript
- Stars
- 2.4k
- Forks
- 329
- Avg merge
- 2d 2h
- Merged PRs (30d)
- 62
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from vercel/chat
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 38/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 Half a day Newbie friendliness 45/100
vercel/chat#1058 · 1 reaction ·
Maintainers usually reply within 1 day
-
Slack adapter: dispatch `app_uninstalled` / `tokens_revoked` to `onUninstalled`Possibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 3/5 1-2 days Newbie friendliness 42/100
Maintainers usually reply within 1 day
-
Proposal: list zoom-chat-adapter as community adapter for Zoom Team ChatPossibly taken @aradhyacp claimed this 1 day ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 35/100
Maintainers usually reply within 1 day
Similar issues
-
bug skills
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
equinor/fusion-framework#5846 ·
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 74/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
Eyevinn/open-videocore#1256 ·
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
[Bug]: Theme preset geist points at undefined --font-geistPossibly taken @claude claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
Maintainers usually reply within 1 day