force-ssl is gone from the requested scope. Its grant ("see, edit, and
permanently delete your videos, ratings, comments and captions") is a strict
subset of auth/youtube's ("Manage your YouTube account"), and Google's own
liveBroadcasts.insert reference lists auth/youtube as an accepted ALTERNATIVE
to force-ssl, so every endpoint this app calls — channels, liveBroadcasts,
liveStreams, liveChat, playlistItems — is covered by the one we keep. Two
consent lines became one, and one fewer scope to justify. Removing a scope
cannot narrow capability when the scope being kept is the broader of the two,
and there is no forced re-consent: a refresh token keeps the scopes it was
originally granted, so existing sessions are untouched.
The playlistItems change is NOT being made, and the map entry recommending it
was wrong. It came out of the verification hunt on the theory that public data
should not ride on user credentials. Walking the code kills it: ChannelStats-
Service is built with one HttpClient, and channels?mine=true on the same
service genuinely needs the session. Splitting it means a second unauthenticated
client PLUS a Google API key, since every YouTube Data API request needs a
credential and the call 403s without one. So the fix would ship a new
extractable, quota-abusable credential inside the binary to replace a token the
app already holds, buying zero verification-surface reduction (the data is
public, no scope involved) and zero correctness. Strictly worse. Recorded as a
do-not-do so it is not "helpfully" reintroduced later.
Test: ExactlyOneScopeIsRequested_AndItIsTheBroadOne asserts on the const's
parsed VALUE rather than scanning the file — a file-wide DoesNotContain
fails on the doc comment that names force-ssl to explain its removal, which is
the same trap I hit and documented earlier today in the locator test. Suite
380/380, 0 warnings.
Reported 2026-09-27: click avatar -> Change Account -> cancel in the Google
chooser -> back to a signed-out app, then "cannot sign-in because the callback
url was not released".
Two independent defects:
1. ChangeAccountAsync ran `await SignOutYouTubeAsync(); await SignInAsync();`.
SignOutYouTubeAsync -> TokenStore.Clear() -> deletes ytLlive.auth, so the
working account's token was destroyed before the browser even opened. It is
now just `await SignInAsync()`: success overwrites the token via the
sessionChanged hook, cancel/failure keeps the current session. The explicit
sign-out was never needed.
2. Abandoning the browser gives the app no signal, so the OAuth attempt held
localhost:8765 for its full 5-minute timeout and a retry could not bind. A
new AuthenticateAsync now cancels and awaits the abandoned attempt first.
That takeover is serialised by a SemaphoreSlim: the first version cleared
the in-flight marker before awaiting, so a caller arriving in that window saw
a free port and raced to bind it -- reproducing the very failure it was
written to prevent. My own test caught that; reasoning had "proved" it fine.
Also adds an `openBrowser` ctor seam so tests never shell out to a real
browser, and corrects the ai.md line that documented "Logout / Change Account ->
SignOutYouTubeAsync" as intended behaviour.
Test: YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort
Full suite 370/370, 0 warnings. Coverage gap noted in HANDOFF.md: the ordering
fix has no automated test because _youtubeAuth is hardcoded in MainViewModel
and ChangeAccountAsync is private.
The reported cross-instance effect is NOT this cause and stays open -- no code
path can sign out a second instance (per-instance TokenStore, local delete, no
server-side revoke).