Skip to content

STU-387: Implement account access actions without fake deletion or allowance resets - #93

Merged
sgardoll merged 3 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-387-implement-account-access-actions-without-fake-deletion-or
Sep 22, 2026
Merged

sgardoll merged 3 commits into
milestone/stu-implement-updated-ui-uxfrom
sgardoll/stu-387-implement-account-access-actions-without-fake-deletion-or

Conversation

@sgardoll

@sgardoll sgardoll commented Sep 22, 2026 •

Copy link
Copy Markdown
Owner

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.

  • Sign out — clears the session (ccc_auth_session) and subscription cache, then synchronously re-renders every identity/plan surface (signed-out panel, email, guest usage, plan). Routed through performSignOut in src/accountAccess.js.
  • Send new link — createSendLinkController reflects 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.
  • Clear cache — clears ONLY the named disposable UI caches (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.
  • Delete account — there is no genuine backend deletion contract in this slice, so the control is a truthful, keyboard-discoverable (real <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

  • Sign out clears the session and immediately updates all identity/plan UI. Unit performSignOut + e2e (stu387-account-access.spec.js "sign out clears...") assert ccc_auth_session/ccc_subscription removed and #auth-signedout visible in the same gesture. Falsified: deleting the key-removal loop made tests 1 & 3 of src/accountAccess.test.js fail; restored → pass.
  • Reauth reflects the actual response and failures; duplicate request prevented. Unit tests 4–7 cover real success/error passthrough and single-send-on-in-flight; e2e covers pending lock + single request. Falsified: removing the if (inFlight) guard made test 6 fail (send reached the endpoint twice); restored → pass.
  • Clear-cache test confirms allowance & identity preserved, cannot grant runs. Unit tests 8–10 assert 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: adding USAGE_KEY to DISPOSABLE_UI_CACHE_KEYS made tests 8–10 fail AND the e2e fail (TypeError: Cannot read properties of null (reading 'count')); restored → both pass.
  • Delete capability truthful and keyboard-discoverable; no active destructive button. Unit tests 11–12 pin DELETE_ACCOUNT_SUPPORTED === false and truthful copy; e2e asserts a real focusable <button> opens a labelled role="dialog" modal, and opening/dismissing it deletes nothing (session/usage unchanged). Falsified: flipping DELETE_ACCOUNT_SUPPORTED to true made 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.
  • Playwright e2e ran on reserved port 3132.

Falsification evidence (per test)

Each new behavioural test was broken, observed to FAIL, restored, observed to PASS:

Criterion Break Break result Restore result
1 Sign out removed session-key removal in performSignOut not ok 1, not ok 3 (session key not cleared) 12/12 pass
2 Reauth dup removed if (inFlight) guard # fail 1 (test 6: duplicate reached sendLink) 12/12 pass
3 Clear cache added USAGE_KEY to disposable keys not ok 8,9,10 + e2e clear-cache failed 12/12 pass + e2e 1 pass
4 Delete DELETE_ACCOUNT_SUPPORTED = true not ok 11 (not honestly unsupported) 12/12 pass

What could NOT be verified

  • No live backend was touched; the reauth/identity/subscription endpoints are asserted against Playwright-routed fixtures only (matching the established repo test contract). The duplicate-prevention controller guard is proven by unit test; the e2e proves the visible pending lock + single request.

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.png
  • screenshots/stu387-delete-dialog-1440x900.png

Full nine-viewport acceptance is owned by the milestone's final review.

…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.
@sgardoll
sgardoll marked this pull request as ready for review September 22, 2026 16:54

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Devin Review

Comment thread app.js
Comment on lines +4518 to +4528
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,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@greptile-apps

greptile-apps Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because pending authenticated operations can restore stale plan state after sign-out or report a magic link against the wrong account.

Fix All in DevinFindings

  1. P1 Sign-out state can return ▶
  2. P1 Link result uses wrong account ▶
Fix with agent prompt
### Issue 1
app.js:4524-4528
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.

### Issue 2
app.js:4555
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.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR implements the STU-387 account Access & data actions, including scoped cache clearing, guarded magic-link requests, synchronous sign-out rendering, and a truthful unavailable account-deletion dialog.

Greptile automatically discovered a related ticket that helped explain the purpose of this PR: account access actions must use real authentication behavior without resetting allowances or pretending to delete accounts.

  • Adds a testable account-access behavior module and unit coverage.
  • Adds account controls, status messaging, and the unavailable-deletion dialog.
  • Adds browser coverage and reference screenshots for the new flows.
  • Two session-transition races remain: stale subscription responses can undo sign-out state, and send-link completion can report the wrong account.

Reviews (1) · Last reviewed commit: "docs(STU-387): add visual slices of the ..."

Comment thread app.js
Comment on lines +4524 to +4528
performSignOut({
storage: window.localStorage,
onIdentityChanged: updateAuthUI,
onPlanChanged: updateSubscriptionUI,
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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

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.

Fix in Claude Code Fix in Codex Fix in Devin

Comment thread app.js
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."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 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.

Fix in Claude Code Fix in Codex Fix in Devin

… 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.
@sgardoll
sgardoll merged commit 0360db6 into milestone/stu-implement-updated-ui-ux Sep 22, 2026
3 checks passed
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