Drop the redundant force-ssl scope, and reject the playlistItems "fix"

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.
This commit is contained in:
2026-09-28 09:45:35 -07:00
parent 4fcf76289c
commit b3edc2e42e
4 changed files with 110 additions and 13 deletions
+44
View File
@@ -1,5 +1,8 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Xunit;
using ytLive.Models;
using ytLive.Services;
@@ -117,4 +120,45 @@ public class YouTubeAuthServiceTests
// The last attempt is deliberately left pending — nothing can cancel it and the
// test host exits before its 5-minute timer fires.
}
/// <summary>
/// The app requests exactly ONE scope, and it is the broad one. 2026-09-28:
/// `auth/youtube.force-ssl` was dropped as redundant — its grant ("see, edit, and
/// permanently delete your videos, ratings, comments and captions") is a strict subset of
/// `auth/youtube` ("Manage your YouTube account"), which is also the only scope that can
/// cover live broadcast create/transition, so it was never avoidable.
///
/// Asserted from source because the const is private and the auth flow cannot be exercised
/// without a browser. What is worth locking in is the DECISION — that the redundant scope
/// does not come back — not merely today's string.
///
/// The assertion reads the const's <b>value</b> out of the source rather than scanning the
/// file. A file-wide `DoesNotContain("force-ssl")` fails on this very file's doc comment,
/// which names force-ssl precisely to explain why it went. A test that fails on the prose
/// documenting the guarantee gets deleted instead of obeyed.
/// </summary>
[Fact]
public void ExactlyOneScopeIsRequested_AndItIsTheBroadOne()
{
var source = File.ReadAllText(RepoFile("Services", "YouTubeAuthService.cs"));
var match = Regex.Match(source, @"private const string Scope = ""([^""]*)"";");
Assert.True(match.Success, "could not find the Scope const in YouTubeAuthService.cs");
var scopes = match.Groups[1].Value.Split(' ', StringSplitOptions.RemoveEmptyEntries);
Assert.Equal(new[] { "https://www.googleapis.com/auth/youtube" }, scopes);
}
private static string RepoFile(params string[] parts)
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
var candidate = Path.Combine(new[] { dir.FullName }.Concat(parts).ToArray());
if (File.Exists(candidate)) return candidate;
dir = dir.Parent;
}
throw new FileNotFoundException(
$"could not locate {Path.Combine(parts)} above {AppContext.BaseDirectory}");
}
}