Repository navigation
STU-387: Implement account access actions without fake deletion or allowance resets - #93
Conversation
…eletion or allowance resets - Sign out clears the session and immediately re-renders identity/plan UI - Send new link reflects the real response/failure, duplicate sends guarded - Clear cache scoped to named disposable UI caches; allowance/identity kept - Delete account presented as truthful, keyboard-discoverable, unavailable - Behavioral unit tests + Playwright spec; all 4 criteria falsification-verified Generated by Claude Code.
… at 360x800 and 1440x900
| authState.email = null | ||
| authState.sessionToken = null | ||
| authState.isVerified = false | ||
| subscriptionState = createSubscriptionState({ isResolved: true }) | ||
| // Remove the session + subscription cache keys (scoped adapter) and then | ||
| // synchronously re-render every identity/plan surface. | ||
| performSignOut({ | ||
| storage: window.localStorage, | ||
| onIdentityChanged: updateAuthUI, | ||
| onPlanChanged: updateSubscriptionUI, | ||
| }) |
There was a problem hiding this comment.
🔴 Late response restores signed-out plan
When handleSignOut runs during fetchSubscription, the pending response restores the old plan. The signed-out browser then exposes its allowance and model access.
Learn more
fetchSubscription applies its response and writes the subscription cache without checking that the session which started the request is still current. handleSignOut clears the current state, but it neither aborts nor invalidates that request. When the response arrives, subscriptionState becomes the former account's tier; updateSubscriptionUI then feeds that tier into allowance and model gating even though the user is signed out.
Example: A Pro user opens the page and the subscription request stalls. They sign out before it returns. The Pro response then lands, and the guest session receives the Pro run limit and model choices instead of Free access.
Recommended fix: Add an auth-session generation or capture the initiating token/email in fetchSubscription. Before applying any response, error, or cache write, verify that the captured session still matches authState; increment the generation on sign-out and session changes. An AbortController can cancel pending work, but retain the identity check because cancellation can race with response completion.
Was this helpful? React with 👍 or 👎 to provide feedback.
|
| performSignOut({ | ||
| storage: window.localStorage, | ||
| onIdentityChanged: updateAuthUI, | ||
| onPlanChanged: updateSubscriptionUI, | ||
| }) |
There was a problem hiding this comment.
If a subscription request is already pending when the user signs out, its response still restores subscriptionState and rewrites ccc_subscription. The generation path can then render and use the former paid tier while the user is signed out. Sign-out needs to invalidate or ignore responses that belong to the ended session.
Greptile automatically discovered a related ticket stating that sign-out must clear the session and immediately update all identity and plan UI, which informed this comment.
Source Used: Linear — Implement account access actions without fake deletion or allowance resets
Prompt To Fix With AI
This is a comment left during a code review.
Path: app.js
Line: 4524-4528
Comment:
**Sign-out state can return**
If a subscription request is already pending when the user signs out, its response still restores `subscriptionState` and rewrites `ccc_subscription`. The generation path can then render and use the former paid tier while the user is signed out. Sign-out needs to invalidate or ignore responses that belong to the ended session.
Greptile automatically discovered a related ticket stating that sign-out must clear the session and immediately update all identity and plan UI, which informed this comment.
**Source Used:** Linear — [Implement account access actions without fake deletion or allowance resets](https://linear.app/stuart-gardoll/issue/STU-387/implement-account-access-actions-without-fake-deletion-or-allowance)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| btn.disabled = false | ||
| btn.textContent = "Send new link" | ||
| msg.className = "acct-access-msg" | ||
| msg.textContent = getMagicLinkResultMessage(state.data, authState.email || "") || "Link sent — check your email." |
There was a problem hiding this comment.
Link result uses wrong account
The request sends to the email captured when it starts, but this line formats the success message using the current authState.email. If the user signs out or changes accounts before the request finishes, the status claims the link went to a blank or different address, and that stale message can reappear in the next signed-in session. Preserve the request-time email in the result and clear this status when the session changes.
Greptile automatically discovered a related ticket stating that reauthentication must reflect the actual response and prevent duplicate requests, which informed this comment.
Source Used: Linear — Implement account access actions without fake deletion or allowance resets
Prompt To Fix With AI
This is a comment left during a code review.
Path: app.js
Line: 4555
Comment:
**Link result uses wrong account**
The request sends to the email captured when it starts, but this line formats the success message using the current `authState.email`. If the user signs out or changes accounts before the request finishes, the status claims the link went to a blank or different address, and that stale message can reappear in the next signed-in session. Preserve the request-time email in the result and clear this status when the session changes.
Greptile automatically discovered a related ticket stating that reauthentication must reflect the actual response and prevent duplicate requests, which informed this comment.
**Source Used:** Linear — [Implement account access actions without fake deletion or allowance resets](https://linear.app/stuart-gardoll/issue/STU-387/implement-account-access-actions-without-fake-deletion-or-allowance)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.… the real failure reason Address two review findings on the account Access & data slice: 1. The 'second send refused' e2e never attempted a second send, so it passed even with the controller's in-flight guard deleted. It now fires the send handler twice in one synchronous tick (the first deterministically sets the in-flight flag before the second call runs) and asserts exactly one request reaches the endpoint. The fixture no longer passes a never-resolving Promise into route.fulfill, which silently aborted the first request and was resetting the controller's in-flight state. 2. The reauth error UI threw away the specific failure reason, so a 429 rate-limit and a 500 read identically. renderSendNewLinkState now surfaces the propagated user-safe reason (e.g. 'HTTP 429' vs 'HTTP 500'), falling back to the generic copy only when no reason escaped, and a new e2e asserts rate-limit and server-error copy differ.
0360db6
into
milestone/stu-implement-updated-ui-ux
STU-387 — Implement account access actions without fake deletion or allowance resets
Linear: https://linear.app/stuart-gardoll/issue/STU-387
What this does
Wires the account Access & data section through the EXISTING auth behaviour, with no fake deletion and no allowance resets.
ccc_auth_session) and subscription cache, then synchronously re-renders every identity/plan surface (signed-out panel, email, guest usage, plan). Routed throughperformSignOutinsrc/accountAccess.js.createSendLinkControllerreflects the REAL backend response or failure into the row's status line (pending → sent/error) and hard-blocks a duplicate request while one is in flight.hasSeenWalkthrough). Authoritative allowance (ccc_usage), identity (bs_identity), credentials (ccc_api_key_*/ccc_session_api_key_*) and the active session are provably preserved — clearing cannot hand out new runs.<button>, opens a labelled dialog) UNAVAILABLE control with copy that names the reason and explicitly says nothing was removed. No destructive action exists and none is claimed.Acceptance criteria
performSignOut+ e2e (stu387-account-access.spec.js"sign out clears...") assertccc_auth_session/ccc_subscriptionremoved and#auth-signedoutvisible in the same gesture. Falsified: deleting the key-removal loop made tests 1 & 3 ofsrc/accountAccess.test.jsfail; restored → pass.if (inFlight)guard made test 6 fail (send reached the endpoint twice); restored → pass.ccc_usage,bs_identity, session and credentials survive and the usage count is unchanged (cannot hand out new runs). e2e "clear cache" asserts the same in the real page. Falsified: addingUSAGE_KEYtoDISPOSABLE_UI_CACHE_KEYSmade tests 8–10 fail AND the e2e fail (TypeError: Cannot read properties of null (reading 'count')); restored → both pass.DELETE_ACCOUNT_SUPPORTED === falseand truthful copy; e2e asserts a real focusable<button>opens a labelledrole="dialog"modal, and opening/dismissing it deletes nothing (session/usage unchanged). Falsified: flippingDELETE_ACCOUNT_SUPPORTEDtotruemade test 11 fail; restored → pass.Verification (full CI sequence — all green locally)
npm test→ 318 passed / 0 failed (baseline on this branch includes the 12 new STU-387 tests).npm run build→ success (37 modules).npm run test:ui(Playwright, full browser suite) → 89 passed.npm run test:accessibility→ passed (7 modal shells, 360×500).npm run test:deploy-guard→ 9/9 passed.npm run test:proxies→ 30/30 passed.python3 scripts/scan_bundle_for_leaks.py→ no leaks.Falsification evidence (per test)
Each new behavioural test was broken, observed to FAIL, restored, observed to PASS:
performSignOutnot ok 1, not ok 3(session key not cleared)if (inFlight)guard# fail 1(test 6: duplicate reachedsendLink)USAGE_KEYto disposable keysnot ok 8,9,10+ e2e clear-cache failedDELETE_ACCOUNT_SUPPORTED = truenot ok 11(not honestly unsupported)What could NOT be verified
Screenshots (visual slices)
screenshots/stu387-access-360x800.png(mobile compact 360×800)screenshots/stu387-access-1440x900.png(desktop 1440×900)screenshots/stu387-delete-dialog-360x800.pngscreenshots/stu387-delete-dialog-1440x900.pngFull nine-viewport acceptance is owned by the milestone's final review.