diff --git a/HANDOFF.md b/HANDOFF.md index e836907..d533e4e 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -36,6 +36,34 @@ browser. **Coverage gap:** the *ordering* fix (defect 1) has no automated test hardcoded in `MainViewModel` and `ChangeAccountAsync` is private, so a real test would shell out to the browser. Wiring an auth seam into `MainViewModel` is a deliberate follow-up, not a bug-fix extra. +## ⏭ NEXT STEP (2026-09-27) — Google OAuth verification: open blocker, nothing recorded before + +**Owning account (creator-confirmed): `llamachile.shop@gmail.com` ("gramps").** Cloud project number +`198374416003` (from the ClientId). Open `https://console.cloud.google.com/apis/credentials?project=198374416003` +signed in as that account. Note the project picker defaults to the **"Recent"** tab — switch to +**"All"**. ⛔ Do not confuse this account with the YouTube channel account used in the app. + +**Answer these two, and the whole verification path is known:** +1. OAuth consent screen **Publishing Status** — "Testing" or "In production"? If still *Testing*: + **100 test-user cap and refresh tokens expire every 7 days**, which is exactly why the creator kept + having to re-consent. Both limits vanish on going to production. +2. Is `https://www.googleapis.com/auth/youtube` flagged **sensitive** or **restricted**? Sensitive → + Google review only (free, self-service, weeks). Restricted → adds an **annual CASA security + assessment by a Google-approved third party** (real cost, recurring, "several weeks" per Google). + Google publishes the scope list but **deliberately does not publish this classification** — the + indicator lives only in the console, so it cannot be researched, only looked up. + +**Scope cleanups found (2026-09-27, not yet done — both trivial, both reduce verification surface):** +- `Services/YouTubeAuthService.cs:18` requests `auth/youtube` **+ `auth/youtube.force-ssl`**. + `force-ssl` is **redundant** — Google's liveChat docs list `auth/youtube` as an acceptable + alternative, and it is the broader of the two ("Manage your YouTube account" ⊃ "see/edit/delete + your videos, ratings, comments, captions"). Drop it: one fewer scope to justify, one fewer consent line. +- `Services/ChannelStatsService.cs:107` calls `playlistItems.list` on the creator's own uploads + playlist (`UU…`) — **public data, needs no OAuth scope at all** (API key suffices). It currently + rides on the user's session token. Does not change the requested scope (`auth/youtube` is forced by + `liveBroadcasts.*`/`liveStreams.list` regardless) but that fetch should not need user credentials. +- `auth/youtube` is **unavoidable**: no narrower scope covers live broadcast create/transition. + ## ✅ DECIDED 2026-09-27 — pricing: `$10/mo` subscription, or `$400` lifetime, **no annual** **Creator's ruling, verbatim: *"Let's just go with $10 monthly recurring subscription for option diff --git a/ai.md b/ai.md index c4ff3d4..51473f7 100644 --- a/ai.md +++ b/ai.md @@ -230,6 +230,7 @@ The AI hallucinated through multiple commits that night on background/scene prop ### Current limitations / TODOs - `Helpers/OAuthCredentials.cs` contains the real ClientId/ClientSecret. Auth is complete and the session **persists via Windows DPAPI** (`Helpers/TokenStore.cs` → `%APPDATA%\ytLlive\ytLlive.auth`, CurrentUser scope), reloaded best-effort at startup with a proactive refresh of a near-expiry access token. Sign-in/Change Account lives **inside the Start Stream dialog** (two-state flow — no separate Connect button). Sign-out is **explicit only**, and that means **Logout — NOT Change Account**: `StopStream()` does NOT clear the session — TASK 18's 2026-08-29 reversal ("stopping a recording leaves the creator signed in") — the older "graceful End signs out" line here was stale and is corrected against the code (2026-09-01). `YouTubeAuthService` takes an optional `HttpClient` + `sessionChanged` + `openBrowser` callback (test seams + save hook; services are still constructed in `MainViewModel`). **Change Account must never sign out first** (fixed 2026-09-27): it used to `await SignOutYouTubeAsync(); await SignInAsync();`, and since `SignOutYouTubeAsync` → `TokenStore.Clear()` **deletes** `ytLlive.auth`, cancelling in the Google chooser left the creator logged out of a working account. It is now just `await SignInAsync()` — success overwrites the token via the `sessionChanged` hook, cancel/failure leaves the session untouched. The OAuth flow also **releases its loopback callback port on retry**: abandoning the browser leaves the attempt holding `localhost:8765` for its 5-minute timeout, so a new `AuthenticateAsync` cancels and awaits the abandoned attempt before binding, serialised by a `SemaphoreSlim` (`_authGate`) — without that gate, two overlapping calls both see a free port and race to bind it (the exact failure the takeover prevents) +- **Google Cloud project ownership (recorded 2026-09-27, do not re-hunt):** OAuth ClientId is `198374416003-s8tm3rb5csh91quvlg6mnec6srmrr6md.apps.googleusercontent.com`, so the **Cloud project number is `198374416003`**. That project is owned by the **`llamachile.shop@gmail.com`** ("gramps") Google account. ⛔ **This is NOT the same account as the YouTube channel a creator signs into the app with** — console access and channel identity are separate logins, and assuming they are the same was the trap that stalled the search. Direct link that resolves it in one hop: `https://console.cloud.google.com/apis/credentials?project=198374416003`. If the picker shows no match, it defaults to the **"Recent"** tab — switch to **"All"** before concluding the project is missing. - Scene/source/asset layout + the social bar persist (SQLite, schema v8); the OAuth session persists (DPAPI); the paid-unlock state persists (LayoutStore Settings table — `LicenseKey`/`LicenseValidatedAt`/`IsPremium`, 14-day offline grace) - `YouTubeStreamService` manages the **variable reusable stream** (shipped 2026-08-16): `GetOrCreateReusableStreamAsync` lists `liveStreams?mine=true` and reuses the existing `cdn.isReusable` stream, creating it only on first use (`resolution=variable`, `frameRate=variable`); the stream is cached via `LayoutStore` (`SaveReusableStream`/`LoadReusableStream`, Settings table) and bound at broadcast insert (`boundStreamId`). Health (shipped 2026-08-16): `GetStreamHealthAsync(streamId)` polls `liveStreams?part=status` for `healthStatus` + `configurationIssues[]` → `StreamHealth`; the real API nests them under `status.healthStatus = {status, lastUpdateTimeSeconds, configurationIssues[]}` — the parse accepts that object shape and the legacy flat-string shape (fix 2026-09-22: the object shape crashed every poll with `requires an element of type 'String'`); banner decision in pure `StreamHealthReporter` - Webcam capture is shipped (milestone 1); the live desktop/game backdrop is shipped (ship task #1); **the output compositor (TASK 4 ship step 1) is SHIPPED**, **the FFmpeg locator (TASK 4 ship step 2) is SHIPPED**, **the encoder + RTMP push (TASK 4 ship step 3) is SHIPPED**, **WASAPI audio capture (TASK 4 ship step 4) is SHIPPED**, **frame-pipeline wiring (TASK 4 ship step 5) is SHIPPED**, **health stats (TASK 4 ship step 6) is SHIPPED**, **one-click go-live + private-only enforcement (TASK 4 ship step 7) is SHIPPED** — TASK 4 (RTMP Ingest) is fully done; full plan in `TASKS.md`