Skip to content

improvement(mship): sweep spent OAuth chat attempts out of localStorage - #6467

Closed
waleedlatif1 wants to merge 3 commits into
stagingfrom
chore/oauth-chat-attempt-storage-sweep
Closed

improvement(mship): sweep spent OAuth chat attempts out of localStorage#6467
waleedlatif1 wants to merge 3 commits into
stagingfrom
chore/oauth-chat-attempt-storage-sweep

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Chat OAuth connect wrote an attempt record to localStorage on every click and never removed one, so they accumulated for the life of the browser profile. Reads already refuse an attempt past the 15m cutoff, so the residue was inert — just unbounded.
  • Collect them on create: resolved records go at the read cutoff, still-pending ones only after a 24h abandoned grace.
  • The grace is the point. readOAuthChatAttempt applies no age gate, so a consent window parked past 15m can still publish a verdict through setOAuthChatAttemptStatus. Sweeping on age alone — what oidc-client-ts clearStaleState does — would drop that record and strand the chip on "pending" with no event to correct it. There is a test that fails under the age-only version.
  • Keys are collected before any removal; localStorage is index-addressed, so removing mid-scan shifts every later entry down a slot and skips it.

Context: CodeQL flagged this file for js/clear-text-storage-of-sensitive-data (alert #471). That was a false positive and is dismissed — the write stores a random correlation nonce, and the cited flows carry credential row IDs, not tokens (GET /api/credentials whitelists columns, never joins account, omits encryptedServiceAccountKey). This PR does not change the flagged line; it fixes the real storage-hygiene gap found while checking the alert.

Type of Change

  • Improvement

Testing

bun run type-check clean. lib/credentials suite 310/310 pass, including 4 new sweep tests. Verified each new test fails without the change it guards — the sweep tests against a no-op prune, and the pending-guard test against an age-only sweep.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

Attempt records were written on every chat OAuth connect and never removed, so
they accumulated in the store for the life of the browser profile. Reads
already refuse an attempt past the 15m cutoff, so the residue was inert — but
unbounded.

Collect them on create instead: resolved records go at the read cutoff, and
still-pending ones only after a 24h abandoned grace. The grace matters —
readOAuthChatAttempt applies no age gate, so a consent window parked past 15m
can still publish a verdict, and sweeping on age alone (what the common OIDC
clients do) would drop that record and strand the row on 'pending'.
@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Aug 9, 2026 1:47am

Request Review

@cursor

cursor Bot commented Aug 9, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes OAuth chat client storage lifecycle and timing edge cases (late popups, mounted chips); well-tested but incorrect pruning could strand UI state or drop valid verdicts.

Overview
Chat OAuth connect wrote attempt records to localStorage on every click and never removed them. This adds retention-based pruning on each new attempt so stale records and their latest-pointer keys do not accumulate for the life of the browser profile.

Retention (24h) is intentionally longer than the existing 15m lookup cutoff (OAUTH_CHAT_ATTEMPT_MAX_AGE_MS). Resolved attempts that mounted chips still read by id stay until 24h after last activity; pending attempts can still receive a late popup verdict via setOAuthChatAttemptStatus even when lookups ignore them. Records track resolvedAt when status leaves pending, and pruning ages from resolvedAt ?? requestedAt so a late connected/failed verdict is not swept immediately.

Pruning runs in createOAuthChatAttempt (not on resolve) to avoid racing chip readers after a verdict. Keys are collected before removal to avoid the localStorage index-shift skip bug when deleting many adjacent entries.

Reviewed by Cursor Bugbot for commit 1652f4e. Configure here.

Comment thread apps/sim/lib/credentials/oauth-chat-attempt.ts Outdated
@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds bounded retention for OAuth chat-attempt records while preserving pending and recently resolved flows.

  • Prunes expired or malformed attempt records and their associated latest pointers when a new attempt is created.
  • Tracks resolution time so late OAuth verdicts receive a fresh retention window.
  • Adds coverage for retention boundaries, late verdicts, pointer cleanup, and adjacent multi-record sweeping.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
apps/sim/lib/credentials/oauth-chat-attempt.ts Adds guarded localStorage pruning and resolution timestamps while retaining active OAuth attempt behavior.
apps/sim/lib/credentials/oauth-chat-attempt.test.ts Adds comprehensive sweep coverage, including an adjacent-record case that resolves the prior review finding.

Reviews (3): Last reviewed commit: "fix(mship): retain attempt records for a..." | Re-trigger Greptile

Comment thread apps/sim/lib/credentials/oauth-chat-attempt.test.ts Outdated
… was requested

Review round 1 findings.

A pending attempt held by the 24h grace is by definition older than the 15m
cutoff, so the moment a late verdict landed on it the record became instantly
sweepable and the next create erased it. Every chip re-reads its row on the
event create dispatches, so a connected chip dropped straight back to unset --
the grace period was defeating its own purpose.

Resolved records now age from resolvedAt, giving the UI the full window to
observe a verdict however late it arrives. Legacy records without the field
fall back to requestedAt.

Also makes the multi-record sweep test a real guard: its records are written
directly so they occupy consecutive storage slots. The earlier version
interleaved latest-pointers between them, which masked the index shift -- a
remove-during-scan regression passed it.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d5a22f4. Configure here.

The 15m resolved cutoff was still short enough to pull a record out from
under a mounted row. A chip recomputes itself from storage on every attempt
event, and on the reconnect path connectedFromWorkspaceChange is forced false,
so connected collapses to exactly the stored status. Any connect click 15m
after a reconnect swept that record -- via the CustomEvent in the same tab or
the storage event these removals now fire in others -- and reverted the row
from Connected. Non-reconnect rows kept the label but silently lost their lock.

OAUTH_CHAT_ATTEMPT_MAX_AGE_MS governs what a lookup honours, not how long a
record has readers. Retention is now one flat window measured from last
activity, which also drops the status branch.
@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

@cursor review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1652f4e. Configure here.

@waleedlatif1
waleedlatif1 deleted the chore/oauth-chat-attempt-storage-sweep branch August 9, 2026 09:08
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