Creator-confirmed: the Cloud project (number 198374416003, from the ClientId) is owned by llamachile.shop@gmail.com. Recorded because finding it cost a hunt, and because the trap was assuming it was the same account as the YouTube channel used in the app -- they are separate logins. Also records what the repo had nothing on at all: the OAuth verification path, the Testing-mode 100-user cap + 7-day refresh-token expiry that explains the repeated re-consent, the sensitive-vs-restricted fork (a paid annual CASA assessment if restricted), and two scope cleanups -- auth/youtube.force-ssl is redundant, and ChannelStatsService's playlistItems.list needs no OAuth scope at all. Docs only, no code touched.
215 lines
15 KiB
Markdown
215 lines
15 KiB
Markdown
# HANDOFF — current state
|
||
|
||
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
|
||
**Last pushed:** `e7cded6` (pricing/Store fee docs). **Local, unpushed:** `536676f` (recording
|
||
filename ms) and the Change Account fix in this unit — ask before pushing.
|
||
|
||
## 🚨 FIXED 2026-09-27 — "Change Account" no longer signs you out before the browser opens
|
||
|
||
**Reported as:** *"I clicked on my signed-in account pic, then clicked sign-in to another account. It
|
||
opened the browser page to login under a different account and I canceled the request. But when
|
||
returned to the app, I see logged out — and should not have happened! Cannot sign-in because the
|
||
callback url was not released."* Also reported as hitting **both** running instances.
|
||
|
||
**Two independent defects, and the second one was masking a race:**
|
||
|
||
1. **The sign-out ran first.** `ChangeAccountAsync` was `await SignOutYouTubeAsync(); await
|
||
SignInAsync();`. `SignOutYouTubeAsync` → `TokenStore.Clear()` → **deletes** `ytLlive.auth`. So the
|
||
working account's token was destroyed *before the browser even opened*; cancelling the chooser left
|
||
nothing. Now `ChangeAccountAsync` is just `await SignInAsync()` — success overwrites the token via
|
||
the `sessionChanged` hook, cancel/failure keeps the current session. No sign-out is needed at all.
|
||
2. **The retry couldn't rebind the callback port.** Abandoning the browser gives the app no signal, so
|
||
the attempt held `localhost:8765` for its full 5-minute timeout. A new `AuthenticateAsync` now
|
||
cancels and awaits the abandoned attempt before binding, **serialised by a `SemaphoreSlim`**
|
||
(`_authGate`) — without the gate, the takeover window let a third caller see a free port and race
|
||
to bind it, i.e. the takeover reproduced the original failure. My own test caught this.
|
||
|
||
**The cross-instance claim was NOT the cause** and is still open as a *separate* question: no code path
|
||
can sign out a second instance (`TokenStore` is per-`InstanceProfile.DataRoot`, `Clear()` is a local
|
||
file delete with no server-side revoke, and no second instance had a token at inspection time). Verify
|
||
both processes carried distinct `YTLIVE_INSTANCE` values and ran Debug builds before chasing it.
|
||
|
||
Test: `YouTubeAuthServiceTests.RetryingAfterAnAbandonedAttempt_ReleasesTheCallbackPort`. Full suite
|
||
**370/370**, 0 warnings (build `ytLive.Tests.csproj`, not just the app project, or VSTest runs a stale
|
||
test DLL). Added an `openBrowser` ctor seam to `YouTubeAuthService` so tests never launch a real
|
||
browser. **Coverage gap:** the *ordering* fix (defect 1) has no automated test — `_youtubeAuth` is
|
||
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
|
||
A and, for option B, a lifetime sub for $400. No annual."* ⛔ **No `.99` prices** (earlier
|
||
ruling: *"Let's stop with the x.99 stuff - I find that irritating. Just say: ten bucks a
|
||
month"*). This **supersedes** the 2026-09-21 one-time `$29 → $49` model and the old `$99/yr`
|
||
sub. No feature gating: free gets everything, the branding flash is the only paid delta.
|
||
|
||
**✅ Store revenue share VERIFIED — 15%, and subscriptions are NOT penalised.** Read from the
|
||
ADA v8.10 PDF itself (downloaded, text-extracted — not a search excerpt). §6(b)(i) charges
|
||
**15%** on "any Apps (and any In-App Products in such Apps) that are **not listed in Section
|
||
6(b)(iii)**"; 12% is Games-only; 30% covers Xbox console apps/games, Xbox non-subscription
|
||
IAP, and Windows 8/Phone 8. LlamaCasty is a Windows PC App — not a Game, not Xbox, not Win8 —
|
||
so **6(b)(i) governs both tiers.** The agreement's own changelog (**v8.0, Oct 26 2017**) says
|
||
it outright: *"implement the **85/15 revenue share for non-Game subscriptions**."*
|
||
|
||
⇒ **`$10/mo` nets `$8.50/mo` (~$102/yr). `$400` lifetime nets `$340`.** Also confirmed: the
|
||
15% applies *after* VAT/GST (Net Receipts), payouts are monthly above a **$50** threshold, and
|
||
no better small-indie rate was found in the standard terms.
|
||
|
||
**⛔ Two obligations §6(h) attaches to the recurring tier** (why lifetime is the hedge):
|
||
(1) we must **fulfil the subscription for the entire period as marketed**, and on breach
|
||
Microsoft may refund the customer *"the full amount, plus taxes... in Microsoft's sole
|
||
discretion"*; (2) **raising the price disables auto-renew** — so `$10` is effectively locked
|
||
for the product's life absent Microsoft's price-change process.
|
||
|
||
**⏳ Still open:** the `$50` subscriber→lifetime upgrade-discount SKU was discussed and is
|
||
**not approved** — do not declare it. Individual vs Company Partner Center account still
|
||
unanswered (get it right before enrolling).
|
||
|
||
## ✅ DECIDED 2026-09-27 — distribution is the Microsoft Store: MSIX + Store IAP
|
||
|
||
**Creator's criteria, verbatim: "zero headaches, minimal maintenance (for me) while still
|
||
providing accountability and a reasonably easy upgrade flow."** Route A is the only option
|
||
where all four are solved by handing the work to Microsoft rather than to a certificate
|
||
vendor: **$0/yr, no certificate, no HSM, no annual renewal, no SmartScreen ramp**, plus Store
|
||
auto-update, Store-side payments/entitlements/refunds/support, and Microsoft review as the
|
||
accountability layer. The rejected options and their reasons are in
|
||
`TASKS/research-store-certification.md` §3 — **read that before reopening the question, not
|
||
before re-deriving it.**
|
||
|
||
**The big consequence: the whole licensing backend is deleted.** `PolarLicenseService`,
|
||
`PolarLicense`, `MainViewModel.License.cs` (`PremiumUrl`, customer portal, the
|
||
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy) and the wrong
|
||
"Polar unlocks alerts" string all become dead code. `IsPremium` comes from the **Store
|
||
entitlement** instead of an HTTP call — which also deletes the entire "network flaky → app
|
||
thinks I'm expired" bug class. **Velopack, the update URL and the DO droplet go too.**
|
||
|
||
**What does not change: the entitlement.** Free gets everything; the branding flash stays the
|
||
ONLY paid delta. Store IAP changes how `IsPremium` is *obtained*, never what it *gates*.
|
||
|
||
**⛔ The old "still open: the price" note is retired — the price is settled, see the top of
|
||
this file.** The old "~$69 floor" and "don't break the launch-price promise" notes refer to
|
||
the old Polar storefront and are **not** current.
|
||
|
||
### The first code unit: bundle ffmpeg (TASK 48 item 1)
|
||
|
||
Two real defects the research surfaced — **neither is fixed yet**, this was a docs unit:
|
||
|
||
1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** Store policy
|
||
**10.2.2** (dynamic code inclusion) verbatim, *and* the root cause of the 2026-09-01
|
||
failure — the pin aged out of BtbN's 14-day retention and the download **404'd on the
|
||
creator's first real recording attempt**. `FfmpegLocator.cs:30-35` records the incident but
|
||
still reads like a design choice. **Fix: bundle it.** Also deletes the startup network
|
||
dependency.
|
||
2. **⛔ `Distribution.md:318` recommended a $400+/yr EV certificate for a SmartScreen bypass
|
||
Microsoft removed in March 2024.** Fixed last commit — had it shipped, it would have cost
|
||
$400+/yr to buy what $150 buys. Now moot anyway (MSIX is signed by Microsoft), but the
|
||
lesson in `MyMistakes.md` stands: a policy citation is a claim about **scope**, not just
|
||
text.
|
||
|
||
### ⛔ Two invariants that break silently if touched
|
||
|
||
- **Full trust, or recording breaks.** `DefaultRecordFolder()` writes to Downloads/MyVideos;
|
||
that passes through unvirtualized **only** at `mediumIL`. Flip the manifest to
|
||
`appContainer` and output vanishes with no error. A comment is owed there **when the
|
||
manifest lands** (TASK 48 item 6) — not before.
|
||
- **Chat is rendered, never stored.** That non-persistence half is the load-bearing part of
|
||
the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload
|
||
may be added without re-arguing 11.12 first.
|
||
|
||
## Landmines
|
||
|
||
- **Stale fact inside `TASKS.md` (left alone, flagged here per the Scope Lock).** The "1.0 gates"
|
||
section asserts "**TASK 36 is now shipped**", but the catalog row 36 reads **☐ Queued** and
|
||
`task-36-gold-pass.md` still shows items 1–6 unchecked. The line most likely means *item 2*
|
||
(branding flash goes live) shipped at `9761b1d`. **Needs a one-line fix, not a code change** —
|
||
flagging rather than fixing because it is not this unit's job.
|
||
- **A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe`** and broke `dotnet run` with
|
||
MSB3027. Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
||
- **`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` is flaky by
|
||
environment** (needs an interactive desktop session; 2 fail / 1 pass in isolation). **Run the
|
||
suite with ytLive CLOSED.** Not a regression.
|
||
- **`RealAppHost.RunAsync` exists for a reason** — a frame-pump test MUST `await` inside it. A
|
||
blocking wait occupies the one shared STA thread and **hangs the whole suite with no output.**
|
||
Always background a `vstest` run and poll the log; a foreground piped `vstest` returns nothing
|
||
in this shell even on success.
|
||
- **`GlobalHotkeys`: two instances registering the SAME global hotkey** — Windows refuses the
|
||
second. Left alone deliberately.
|
||
- **MSIX writes under a real package identity are unverified.** The docs say full trust passes
|
||
user-profile writes through, but confirm on a real packaged build before trusting it with
|
||
recordings (TASK 48 item 6).
|
||
|
||
## Test state
|
||
|
||
**367/367 as of `938c5b3`.** Not re-run for this unit — docs only, no code touched.
|
||
|
||
## Uncommitted / untracked
|
||
|
||
- `MARCOM.md`, `MONETIZATION.md` — both edited (privacy-copy guard; the Polar-obsolete banner
|
||
and the re-opened-price note) and both **gitignored by design** (confidential business
|
||
files, `.gitignore:10-11`). They stay local, as intended. Same for `CREDENTIALS.md` —
|
||
**never commit those.**
|
||
|
||
## Next
|
||
|
||
1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, first code unit, fixes a real user-facing
|
||
failure and clears the one hard certification gate.
|
||
2. **Settle the price** — one-time vs subscription, and the number. Then declare the IAP
|
||
products in Partner Center. **Blocks:** the Store listing, and the `OverlayHost.xaml` copy.
|
||
3. **The packaging project + `Package.appxmanifest`** (TASK 48 items 2–3) — full trust,
|
||
`webcam`/`microphone`, English only; Velopack deleted. Add the `DefaultRecordFolder()`
|
||
comment in the same unit.
|
||
4. **Polar teardown** (TASK 48 item 0) — `PolarLicenseService`, `PolarLicense`,
|
||
`MainViewModel.License.cs`, the `OverlayHost.xaml` string, the `csproj`/`App.xaml.cs`
|
||
Velopack sites. `IsPremium` ← Store entitlement. **Do this as one unit** — a half-torn-down
|
||
licensing path is worse than either end state.
|
||
5. **Verify the Store revenue-share rate** — before step 2, not after.
|
||
6. **Vertical recording verification** — the compositor tier is proven by
|
||
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
|
||
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
|
||
7. **WACK + certification notes + the privacy policy** (TASK 48 items 4–5) — the policy must
|
||
state camera/mic is OS-gated, nothing is retained, and nothing is fetched at runtime.
|
||
8. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health
|
||
message) when a stream or recording ends. `SessionTeardownTests` is the natural home.
|
||
9. **Measure whether the Windows camera toggle gates DShow** — gates all privacy-forward copy
|
||
(`MARCOM.md` guard). Not a certification risk; a trust risk.
|
||
10. **TASK 36:212 stale "shipped" line** — one-line fix, needs a hand.
|
||
|
||
**Dropped from the list** because the ruling made them moot: multi-instance's ffmpeg tools-dir
|
||
race (item 1 makes it disappear), `scripts/publish.sh` + the `LlamaCasty.exe` rename (the Store
|
||
packages and delivers — *revisit only if we ever ship outside the Store*), and the alert-gating
|
||
copy (the wrong string dies with the Polar teardown in step 4).
|
||
|
||
**Everything else in the v1 queue:** stream resilience (32), bandwidth step-down (33), scheduled
|
||
streams (34), scene-linked audio (35), the master limiter (12), text source (3.16), reward
|
||
events (3.20), the media picker (21), webcam identity (9.5), settings units A/C/D (40), and the
|
||
defaults split (37). Ship-checklist items live in `TASKS.md` -> "1.0 gates".
|