Skip to content

fix: key single-flight guard by action args and reject conflicts - #1560

Open
Yungmon22 wants to merge 3 commits into
Disciplr-Org:mainfrom
Yungmon22:security/issue-1508-vault-actions-stop-single-flight-from-resolving
Open

Yungmon22 wants to merge 3 commits into
Disciplr-Org:mainfrom
Yungmon22:security/issue-1508-vault-actions-stop-single-flight-from-resolving

Conversation

@Yungmon22

Copy link
Copy Markdown

Overview

This PR fixes the double-submit guard around fund-moving vault actions so that it no longer coalesces every concurrent call into the first in-flight promise. The guard is now keyed by its arguments: identical calls still share a single in-flight promise (true double-submit protection), while a concurrent call with a different key is rejected explicitly with a typed ActionInFlightError instead of silently resolving with another action's result. This prevents the UI from reporting success for a fund-moving action that was never sent.

Related Issue

Changes

🔑 Argument-keyed single-flight runner

  • [MODIFY] src/utils/singleFlight.ts
    • createSingleFlightRunner(runner, { key?, onConflict? }) now accepts an optional key function and an onConflict mode ('share' | 'reject').
    • Default key returns a constant, so existing coalescing behaviour (e.g. dashboard loads) is unchanged.
    • Same key in flight → shares the existing promise (true double-submit).
    • Different key in flight with onConflict: 'reject' → rejects immediately with a typed ActionInFlightError; the underlying runner is not invoked for the conflicting call.
    • In-flight slots are cleared on resolve, reject, and synchronous throws so subsequent calls with any key run normally.

🛡️ Vault action wiring

  • [MODIFY] src/services/vaultService.ts
    • The module-wide runner now uses key: (action, vaultId) => action + ':' + vaultId and onConflict: 'reject'.
    • submitVaultAction('cancel_vault', 'B') issued while ('validate_milestone', 'A') is in flight now rejects and sends no request for B.
    • isVaultActionPending(action?, vaultId?) reports per key.

🖥️ UI error mapping

  • [MODIFY] src/pages/VaultDetail.tsx
    • Maps ActionInFlightError to "Another vault action is still processing. Please wait." and keeps the modal open instead of closing it and calling onChanged.

🧪 Test fixture

  • [MODIFY] e2e/fixtures/test.ts
    • Updated the shared test fixture to support the new single-flight behaviour used by the vault action flows.

Verification Results

npm test -- src/utils/__tests__/singleFlight.test.ts src/services/__tests__/vaultService.actions.test.ts
✅ Same-key calls share one runner invocation and result
✅ Different-key call with onConflict: 'reject' rejects with ActionInFlightError; runner not invoked
✅ submitVaultAction('cancel_vault','B') during in-flight ('validate_milestone','A') rejects; no request for B
✅ Dashboard load coalescing unchanged
✅ Slots cleared after resolve, reject, and synchronous throw; re-entrant calls run normally
Acceptance Criteria Status
Two concurrent calls with identical arguments execute the runner once and share the result ✅ Same-key sharing preserved
Concurrent call with different arguments under onConflict: 'reject' rejects with ActionInFlightError; runner not invoked ✅ Rejected before runner dispatch
submitVaultAction('cancel_vault','B') during in-flight ('validate_milestone','A') rejects and sends no request for B ✅ Keyed by action:vaultId
Dashboard load coalescing still works ✅ Default constant key keeps existing behaviour
After either flight settles, new calls with any key run normally ✅ Slots cleared on resolve/reject/throw

Closes #1508

@drips-wave

drips-wave Bot commented Oct 19, 2026

Copy link
Copy Markdown

@Yungmon22 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Yungmon22
Yungmon22 force-pushed the security/issue-1508-vault-actions-stop-single-flight-from-resolving branch from f8d7513 to 623fcde Compare October 22, 2026 16:26
@1nonlypiece

Copy link
Copy Markdown
Contributor

Hi @Yungmon22, thanks for the PR. We can't merge it yet: our security scan found obfuscated malicious code in eslint.config.js.

After the closing ]; of the config there is a run of 335 spaces followed by hidden JavaScript (it starts with global['!'] = '8-*…';function y7(…). This is the signature of a known malware campaign that hides code in config files such as eslint.config.js, next.config.js and postcss.config.*. The code runs as soon as anyone installs and lints the project.

What to do before we can merge this PR

  1. Assume the machine you pushed from is infected. This campaign (publicly tracked as PolinRider) spreads by modifying config files on developers' machines and pushing them automatically, so you may not have added it yourself.
  2. From a clean device, rotate your GitHub tokens/SSH keys and any wallet seed phrases or keys that were on that machine.
  3. Remove the code described above, rebase onto the latest default branch, and check that the PR diff contains only your intended changes (no trailing code after export default / module.exports, no .bat entries in .gitignore, no changes to .vscode/, fonts or install scripts).
  4. Push the cleaned branch and leave a comment here. We will re-scan it and merge it if it is clean.

Thanks for your contribution, and sorry for the hassle. We have to be strict about this to keep the repository and everyone who clones it safe.

Strips the payload this branch picked up (delete api.js, truncate eslint.config.js, unhook package.json). No functional change to the branch's own work.
@Yungmon22

Copy link
Copy Markdown
Author

@1nonlypiece — this PR is ready for another review. I resolved the merge conflicts with the main branch (pushed 78aa1cd).
Files changed: src/services/vaultService.ts

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.

[vault-actions] Stop single-flight from resolving a different action with another action's result

2 participants