diff --git a/Distribution.md b/Distribution.md index 64f6b0f..4c42788 100644 --- a/Distribution.md +++ b/Distribution.md @@ -315,7 +315,36 @@ Single-file means everything is inside the `.exe`. No DLLs, no runtime dependenc ### 4.3 Code Signing -**Certificate:** Buy an EV (Extended Validation) code-signing certificate from DigiCert or Sectigo ($200-400/year). EV certificates get instant SmartScreen reputation; DV/OV certs trigger SmartScreen warnings for months. +> ⚠️ **The cert choice below is a live decision, and the old plan's EV recommendation is +> dead.** Microsoft **removed the SmartScreen instant-bypass for EV certificates in March +> 2024** — an EV-signed file now accrues reputation exactly like an OV-signed one, so paying +> $400+/yr buys precisely what paying ~$150 buys. **Do not buy EV.** +> +> **But the more interesting finding is that you may not need a certificate at all.** Three +> costs, current as of 2026-09-27: +> +> | Route | Cert/yr | SmartScreen | Note | +> |---|---|---|---| +> | **Microsoft Store (MSIX)** | **$0** — Microsoft re-signs | **No warning** | Requires MSIX packaging; Store review; public listing | +> | **Microsoft Store (EXE/MSI)** | $120–300 — *your* cert | Warning ramp | Store does **not** re-sign; silent install mandatory. Strictly dominated — cert required anyway | +> | **Direct + OV cert** | $150–300 | Warning ramp | Worldwide availability | +> | **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120 | Warning ramp | **Individuals: USA/Canada only.** Identity validation required | +> +> Signing does **not** clear the warning on day one either way: a valid cert stops the +> *malware* warning immediately, but "unrecognized publisher" clears as SmartScreen accrues +> reputation from real download volume. Budget cert validity at **460 days** (CA/B Forum +> CSC-31) — this is a recurring line item, not a one-time purchase — and note that private +> keys must sit on an **HSM or hardware token**. +> +> **The full comparison, the four real combinations, and the policy analysis live in +> [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) §2–3. +> The executable checklist is [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md), +> and the route decision is the creator's.** + +**Certificate:** Buy an **OV (Organization Validation)** code-signing certificate from DigiCert +or Sectigo (~$150–300/year), or an **Azure Artifact Signing** profile (~$9.99/month, +USA/Canada individuals only) if the geography allows. ⛔ **Not EV** — no SmartScreen benefit +since March 2024. **Or buy nothing at all** if the route is the Microsoft Store. **Signing process:** ```powershell diff --git a/HANDOFF.md b/HANDOFF.md index fe86104..070bb5b 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,146 +1,127 @@ # HANDOFF — current state -**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`. -**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main` -(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so. +**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24). +**Last pushed:** `938c5b3` (alert ticker). This unit (distribution/certification research) is +**docs-only** — no code touched, no build, no tests run. -## The 2026-09-26 proof-of-concept round (creator reporting) +## This unit: distribution & Store certification research is WRITTEN DOWN -Context: **no YouTube private test recordings are being saved**, so the creator is judging -compositing from **local recordings**. They assumed the live path needed a separate "redirect" — -**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the -`MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68` -(go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two -outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch". +The session's open question was "how do we get this in front of users without a SmartScreen +scarewall" and it had been answered **in conversation only** — which meant the next session +would re-derive it. It is now in the map, and the conversation can be dropped. -### Done: the alert ticker draws inside the Stream Alerts box +| File | What it is | +|---|---| +| `TASKS/research-store-certification.md` | **new** — the authoritative research. Store Policies **7.20** (effective 2026-10-22, re-check the version), MSIX full-trust vs AppContainer, cert economics, a ⛔/✅ table of which policies bind and which don't, the camera/mic gating layers, the YouTube age + COPPA analysis, the 11.12 UGC judgment call, and the filter design constraints | +| `TASKS/task-48-distribution-msix.md` | **new** — the executable checklist. Carved out of TASK 36 item 6 (code signing / installer / Velopack URL) so distribution has one owner | +| `TASKS/task-49-chat-profanity-filter.md` | **new** — the chat filter checklist. Not blocked | +| `Distribution.md` §4.3 | **corrected** — the EV recommendation was dead (§ below) | +| `ai.md` | new "Windows packaging & distribution" section (durable invariants) + a correction on the FFmpeg locator | +| `MyMistakes.md` | the policy-applicability lesson | +| `TASKS.md` | rows 48/49, a research index, and the TASK 36 item 6 split | +| `MARCOM.md` | ⛔ **privacy-copy guard — gitignored, so this edit is local-only and did not travel with the push** | -Was a **global 1920px bar pinned to the top edge**; the creator wants it "over the stream alerts -video, not over the entire preview window". The strip is now rendered at the alert box's own size -and the box's origin travels on the frame in a new `VideoFrame.Placement` (`(int X, int Y)?`, with -`OriginX`/`OriginY` defaulting to 0 so full-canvas overlays are unaffected). Every blit site now -reads `tickerFrame.OriginX/OriginY` instead of a literal `0, 0` — `SceneCompositor` ×2 plus -`FramePump`'s static-bake `Overlay` path — and `PreviewPane.xaml`'s `AlertTickerElement` binds the -same rect (`AlertTickerLeft/Top/Width/Height`), so preview and output can't drift. +### Two real defects the research surfaced (both now recorded, neither fixed — no code this unit) -Design notes: the position rides on the frame rather than in a full-canvas frame because a -1920×1080 overlay is 8.3MB of LOH garbage per tick (~2.5GB over one 10s alert); the strip is -~370KB. `CopyStrip` now clips **rows** to the target height — the pill rasterises at 48px, so a -shorter box would have written past the buffer. **No alert box in the scene ⇒ no ticker.** +1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** That is Store + policy **10.2.2** (dynamic code inclusion) verbatim, *and* it is the root cause of the + 2026-09-01 failure: the previous daily 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. → **TASK 48 item 1.** Not + Store-conditional: bundling kills this whole class of cold-start failure and deletes the + startup network dependency. **This is the highest-value unblocked item in the repo.** +2. **⛔ `Distribution.md:318` recommended a $400+/yr EV certificate for a benefit Microsoft + deleted in March 2024.** Fixed. Had it shipped, it would have cost $400+/yr to buy exactly + what $150 buys. Lesson in `MyMistakes.md` — the recurring shape is *citing a policy or a + platform fact from memory instead of re-verifying it in the document itself.* -### Done: the branding flash is no longer a go-live-only behaviour +### ⛔ The decision that is NOT made, and belongs to the creator -The presenter was `Start()`/`Stop()`-ed from `UpdateLiveVisuals()`'s `IsLive` branch, so a -recording made **without ever going live** carried no credit — exactly the creator's complaint. -It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it -runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the -recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes -`BrandFlashPresenter.Enabled`. +**The distribution route.** Research is done and MSIX is the front-runner (full trust is viable; +we trip **none** of the four MSIX disqualifiers — no driver installed, no per-user service, no +elevation, no shell extension). Four real combinations, costed in +`TASKS/research-store-certification.md` §3: -**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`. -When `Start()` was per-go-live, the next go-live restarted it; with one app-lifetime `Start()` -nothing would, so a key entered mid-session would leave the credit dead until the process -restarted. The setter now restarts the timer when re-enabling while `_running`. +| # | Route | Cert/yr | SmartScreen | Notes | +|---|---|---|---|---| +| A | Store MSIX + Store IAP | **$0** | none | Polar gets deleted; revenue cut; public listing | +| B | Store MSIX + **Polar** | **$0** | none | free signing **and** keep 100% — two systems to maintain | +| C | **Polar file hosting** + own cert | $120–300 | warning ramp | **the status quo already sketched in `Distribution.md:14`/`:296`** | +| D | Store EXE (policy 10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway, silent install, own URL | -Test-arithmetic trap, hit twice now: `Advance(5.1)` in one call can never observe a credit — -`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at -1/30s like the real timer (the `Step` helpers). +**Nothing else was allowed to block on this** — the user is right that most of the research is +route-independent and worth banking regardless. So: research banked, dead line fixed, and only +the packaging work is parked. -### Done this unit: recording save dialog — Cancel now discards +**Also still open, deliberately:** pricing (one-time vs one-time+monthly), the license provider, +and therefore all licensing copy. `OverlayHost.xaml` still claims Polar unlocks alerts — wrong, +alerts are not gated. Unresolved, queued, **not** to be papered over. -`RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`). -**The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true -and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording -under the default name. Extracted the decision into internal -`MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`: +### The two findings most likely to be forgotten -- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left - empty. A failed delete returns `DiscardFailed` and the toast names the file + folder. -- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name. - Blank box still means "keep the auto name" — but only on an explicit Save. - -Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the -headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged". -Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect). - -## Two earlier work units (committed) - -### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped) - -It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a -credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to -**both** the frame pump and the preview, so they cannot drift: - -- `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue - halo, channels split opposite), 2s envelope, random on-canvas placement per presentation, - licence gate re-checked on every read. -- `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump + - `PublishBrandFlashPreview()` for the pane. -- `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the - C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path. -- `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame). -- `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable. - -Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every -`rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas. - -### 2. Dev-only: two instances side by side - -`Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=` and that process gets a private -`%APPDATA%\ytLlive\instances//` root for the **layout DB, auth file, startup.log and the -WebView2 user data folder**. Chromium locks that folder exclusively — without this the second -instance does not start at all. Recording folder and the ffmpeg `tools` cache stay shared on -purpose; global hotkeys stay un-namespaced. - -**Usage:** `$env:YTLIVE_INSTANCE=2; dotnet run` - -The entire implementation is inside `#if DEBUG`. Release compiles to `DataRoot => DefaultRoot` + -`WebViewDataFolder => null`; the call sites are unconditional so Release cannot drift. - -## Bugs found and fixed along the way - -- **Pre-existing, not mine:** `FramePump`'s **fully-static shortcut** stretched the cached bake and - returned it, silently discarding **every per-frame overlay** — the social bar and the alert ticker - were already lost there, not just the flash. Now composites overlays onto a copy before stretching. -- **Cadence bug caught by my own test:** the interval was an absolute deadline, not a period, so - gaps collapsed (6.7s / 3.5s / 2.8s instead of 30–60s). Fixed. -- `BrandFlashPresenter` recycles one 8MB master buffer and **must** stamp `VideoFrame.Epoch` per - read, or the paste cache freezes the credit. It does. - -## Test state — VERIFIED - -- **357 total, 356 pass.** 18 new facts (10 brand flash, 8 instance isolation). -- The one failure is `LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder`, - and it is **flaky by environment, not a regression** — 2 fail / 1 pass in isolation. Its own doc - comment: "Requires an interactive desktop session: if the window is covered or the session is - locked, the pointer no-ops and the drag never lands." The symptom matches exactly (order - unchanged). **Run the suite with ytLive CLOSED** (see `MyMistakes.md` 2026-08 era note). -- `RealAppHost.RunAsync` was added for this: a frame-pump test MUST `await` inside it. A blocking - wait in `RealAppHost.Run` occupies the one shared STA thread and hangs the whole suite with no - output. **Always background a `vstest` run and poll the log** — foreground piped `vstest` returns - nothing in this shell even on success. +- **Distribution and licensing are independent.** Policy 10.8.1 lets a **Store-distributed** + app verify licenses with **Polar**. So "should we use the Store?" does not imply "drop Polar?" + Only route A removes Polar, and that is a licensing decision riding on a distribution one. +- **⛴ Two invariants that will 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 -- 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.** -- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second. - Left alone deliberately. -- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued. +- **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` — the privacy-copy guard was added but the file is **gitignored by design** + (confidential business file, `.gitignore:11`). It stays local, as intended. Same for + `MONETIZATION.md` and `CREDENTIALS.md` — **never commit those.** ## Next -1. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm - the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027 - *before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2` - + `dotnet run --no-build` in a second shell is the known-good path. -2. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health - message) when a stream or recording ends. -3. **The creator's own eyes on the output** — the ticker-placement and brand-flash changes are proven - by unit tests and the compositor, but the final proof is a local recording, since no YouTube - private test recordings are being saved. -4. Push the commits when the creator asks. -5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out - input, Enter inserts a newline. -6. Decide the alert-gating copy question above. -7. Ship-checklist items live in `TASKS.md` → "1.0 gates". +1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, recommended on every route, and it fixes a + real user-facing failure. *This is the next code unit.* +2. **The route decision** (creator) — A/B/C above, once the ffmpeg fix is out of the way. +3. **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. +4. **Multi-instance** — route `FfmpegLocator._toolsDir` through `InstanceProfile` (same shared + extraction race as #1, and it becomes moot if ffmpeg is bundled). Confirm the launch method: + `$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path. +5. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health + message) when a stream or recording ends. `SessionTeardownTests` is the natural home. +6. **`scripts/publish.sh` + Debug-only `InternalsVisibleTo` + the `LlamaCasty.exe` rename** — + still queued from 2026-09-26; do **NOT** add `-p:PublishTrimmed=true` (WPF fails at runtime, + not build time). See `TASKS.md` → "Shipping / release build". +7. **The alert-gating copy** (`OverlayHost.xaml`) — fix the copy or gate the alerts; do not leave + it lying. Blocked behind the pricing ruling. +8. **The camera-privacy measurement** — does the Win11 desktop-app camera toggle actually gate a + DShow webcam and a capture card? Gates all privacy-forward copy (`MARCOM.md` guard). +9. **TASK 36:212 stale "shipped" line** — one-line fix, needs a hand. +10. Ship-checklist items live in `TASKS.md` → "1.0 gates". diff --git a/MyMistakes.md b/MyMistakes.md index 393d54c..d8f924b 100644 --- a/MyMistakes.md +++ b/MyMistakes.md @@ -1094,3 +1094,40 @@ the default file name and save the file — that's what pressing enter should do the save option, discarding the recording." A Cancel button on a destructive-and-final dialog means *discard*, not *accept default*. When a dialog is the last gate before data is committed, ask what Cancel should destroy — don't infer "keep the default" just because the default already exists. + +--- + +## 2026-09-27 — A policy number in my head is not a policy finding: check that the policy *applies* + +Two confident, wrong claims in one Store-research pass, both of the same shape — I had a policy +number and a confident paraphrase attached to it, and never asked whether the policy's own scope +clause covered us. + +**1. "Store policy 10.10 (Advertising) requires X"** — 10.10 does not apply to LlamaCasty at +all. Every clause in it is conditioned on **ads your product displays**. YouTube injects ads +*server-side after the RTMP handoff*; the app renders no ad, hosts no ad SDK, and makes no ad +decision. I spent a cycle designing around a rule that was never in force. Same error later with +**10.2.5** (Store-only installation): the current policy text scopes it to **game and Xbox +console products**, and the older v7.7 wording that said "all of your apps" has been narrowed — +citing the stale wording would have produced a confidently wrong conclusion in the *opposite* +direction (claiming we were locked to MSIX). + +**2. "Buy an EV certificate — EV gets instant SmartScreen reputation."** This one was worse than +wrong, because it was already **written into `Distribution.md`** and would have cost $400+/yr. +Microsoft **removed the SmartScreen instant-bypass for EV certs in March 2024**. EV and OV now +accrue reputation identically. I recited a fact that had been dead for over two years, in a +document that already contained the recommendation. + +**The rule, and the generalisation:** *a policy citation is a claim about scope, not just about +text.* Before acting on one, read the **applicability sentence** and answer three questions — +does it cover our product class, does it cover our distribution model, and is the wording the +**current** version? A policy number without a verified scope is a guess wearing a citation's +clothes, and the failure mode is invisible because the sentence still *sounds* authoritative. + +Corollary: **a stale external fact inside a shipped document is worse than a missing one** — it +is trusted, acted on, and costed. When a doc makes a factual claim about the outside world +(certificates, platform behaviour, policy text), re-verify it on touch, not on memory. This is +the same class as the dead-Pin incident in `FfmpegLocator` (`ai.md` → FFmpeg locator): an +external thing changed, our record of it did not, and the mismatch was discovered by a user +rather than by a test. **Records of the outside world rot silently and are believed until they +cost money.** diff --git a/TASKS.md b/TASKS.md index 29ace28..0bd2384 100644 --- a/TASKS.md +++ b/TASKS.md @@ -63,6 +63,17 @@ | 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) | | 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) | | 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | ✅ Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) | +| 48 | Distribution & packaging: route decision, bundled ffmpeg, MSIX + code signing | ⏳ Queued — **blocked on the creator's route decision**; bundled-ffmpeg half is unblocked and recommended | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) | +| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) | + +--- + +## Research index + +| Research | Covers | +|---|---| +| [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) | YouTube Live Streaming API v3 — authoritative facts for the v3 build | +| [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) | **Windows Store policies 7.20 + MSIX packaging + code-signing economics** (2026-09-27). Feeds TASK 48. Records which policies bind, which don't, and why — read it before re-litigating distribution | --- @@ -104,7 +115,7 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw `DroppedFrames`/`StreamDuration`; `SessionTeardownTests` is the natural home for the roll-up. - **TASK 3** — items 16 (Text source) + 20 (RewardEvent capture → SQLite, Alerts' persistence half) open; **17 (Alerts) is DONE via TASK 43 (2026-09-24) — native six-event alert box** - **TASK 9** — item 5 open (webcam identity key reconciliation) -- **TASK 10** — Velopack update URL pending +- **TASK 10** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates) - **Shipping / release build (creator-queued 2026-09-26)** — no publish config exists yet: the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish` is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled). @@ -198,6 +209,27 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw **ticker** ("Funder — Super Chat · $10.00") scrolls along the very top of the frame (never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings: custom + fallback (not either/or), ticker on top, no ducking. +- **TASK 36 item 6 (release engineering) is now TASK 48** (2026-09-27) — code signing, the + installer, and the Velopack update URL have one owner instead of three scattered mentions. + **EULA draft/review and the THIRD-PARTY-NOTICES license-texts gate stay in TASK 36 item 6**, + as does the agreed build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only). +- **TASK 48 — distribution & packaging** — **⏳ the route decision belongs to the creator.** + Research is done and MSIX is the front-runner; the four real combinations (Store+Store IAP / + Store+Polar / Polar-hosted+own cert / dominated Store-EXE) and their cert costs are tabulated + in `TASKS/research-store-certification.md` §3. Two things are settled and unblocked: + **(a) bundle ffmpeg instead of downloading it** — `FfmpegLocator` currently downloads an + unsigned exe from GitHub and runs it, which is Store policy 10.2.2 (dynamic code inclusion) + *and* is the root cause of the 2026-09-01 expired-pin 404; **(b) the `Distribution.md` EV + claim is dead** — Microsoft removed the SmartScreen EV bypass in March 2024, so paying + $400+ buys what $150 buys. Full-trust MSIX is viable (mediumIL, not AppContainer, and the + three technical disqualifiers — drivers, per-user services, elevation — are all clear). + Packaging, Velopack removal, WACK, the demo account, the privacy policy and the IARC + questionnaire are all specified in the task file and waiting. +- **TASK 49 — chat profanity filter** — queued, not blocked, size S. Local, on-device, + **non-persistent**, opt-in, **user-supplied word list (never a hardcoded slur list)**, and it + must **never match the SuperChat amount or reward fields** (financial data, Store 10.5.5). + The non-persistence half is the same property that makes the 11.12 UGC certification answer + strong, so **no future chat-history/moderation-log/analytics feature may quietly break it.** --- diff --git a/TASKS/research-store-certification.md b/TASKS/research-store-certification.md new file mode 100644 index 0000000..e27f3c1 --- /dev/null +++ b/TASKS/research-store-certification.md @@ -0,0 +1,448 @@ +# ytLlive — Windows Store & code-signing certification research (authoritative) + +> Catalog: [`TASKS.md`](../TASKS.md) · memory map: [`schema.md`](../schema.md) · distribution plan: [`Distribution.md`](../Distribution.md) +> +> **Researched 2026-09-27** against **Microsoft Store Policies version 7.20** (published +> 2026-09-15, effective 2026-10-22) and the MSIX packaging docs. Policy version moves — +> re-check the version before acting on any line here. +> +> **This file is the "why".** The executable checklist lives in +> [`task-48-distribution-msix.md`](task-48-distribution-msix.md). No code changed as a +> result of this research; it only decides what task-48 will eventually do. + +--- + +## 1. Bottom line + +**MSIX packaging is viable.** One real blocker, and it is a code change we should want +anyway (§4). Three technical blockers that normally kill a Store submission — drivers, +per-user services, elevation — we **do not trip** (§7). + +**The decision is NOT made.** The route is open; this file records the options and their +costs so the decision can be made from a clear page. `Distribution.md` holds the older +plan and contains **one factually dead claim** — see §2. + +--- + +## 2. ⚠️ The one line in `Distribution.md` that is now wrong + +`Distribution.md:318` currently says: + +> Buy an EV (Extended Validation) code-signing certificate from DigiCert or Sectigo +> ($200-400/year). EV certificates get instant SmartScreen reputation; DV/OV certs +> trigger SmartScreen warnings for months. + +**The EV half of that is false.** Microsoft removed the SmartScreen instant-bypass for EV +certificates in **March 2024**. An EV-signed file now accumulates reputation exactly the +way an OV-signed file does. Paying $400+/yr buys precisely what paying $150 buys. + +`schema.md` rule 4: stale facts get FIXED, not appended. Corrected in `Distribution.md` +(2026-09-27). + +Also worth knowing before budgeting: + +- **Cert validity is capped at 460 days** (CA/B Forum CSC-31, effective 2026-03-01). This + is a recurring annual line item, not a one-time purchase. +- **Private keys must live on an HSM or hardware token** (CA/B rule since 2023-05). Budget + for the token or a cloud HSM. +- **Signing does not clear the warning on day one.** A valid cert stops the *malware* + warning immediately; the "unrecognized publisher" warning clears as SmartScreen accrues + reputation from real download volume. Expect a ramp on every route. +- **Third-party AV vendors layer their own reputation blocking on top of SmartScreen**, so + signed-but-new can still warn on consumer machines. The Store route is the only one that + is warning-free, because Microsoft holds the reputation. + +--- + +## 3. Signing economics + +| Route | Cert cost | SmartScreen | Install UX | Notes | +|---|---|---|---|---| +| **Microsoft Store, MSIX package** | **$0** — Microsoft re-signs | No warning | Normal | Store review, public listing, updates handled by Store | +| **Microsoft Store, EXE/MSI (10.2.9)** | **$150–300/yr — your own cert** | Warning ramp | **Silent install only** | Store does NOT re-sign; you maintain a versioned URL | +| **Direct + OV cert** | $150–300/yr | Warning ramp | Yours | Worldwide availability | +| **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120/yr | Warning ramp | Yours | **Individuals: USA/Canada only.** Identity validation required | +| **Direct + EV cert** | $400+/yr | Same as OV | Yours | **Do not buy.** No benefit since March 2024 | +| **Unsigned direct** | $0 | Strong block | Yours | Unusable for public download | + +### Four real combinations + +| # | Distribute | Verify payment | Cert | Revenue cut | +|---|---|---|---|---| +| **A** | Store | Store IAP | $0 | yes | +| **B** | Store | **Polar** | $0 | no | +| **C** | Polar file hosting | Polar | $150–300/yr | no | +| **D** | Store EXE (10.2.9) | Polar | $150–300/yr | no | + +**A** is the only one where Polar becomes unnecessary — the Store handles payment, +entitlement, and refunds, and you would delete `PolarLicenseService` and the +customer-portal plumbing. That is a *distribution* choice that happens to subsume +licensing. + +**B** is the compromise worth serious consideration: the Store solves the SmartScreen +problem and the annual cert bill while Polar stays the licensing system and we keep 100% +of revenue. Cost is two systems to maintain. + +**C** is the status quo already sketched in `Distribution.md:14` / `:296` — Polar hosts the +signed exe (10GB, signed personal download URLs, SHA-256 included). **The Store is not +needed for delivery at all.** The Store's only unique value is *free Microsoft signing with +no SmartScreen warning*, and its cost is MSIX packaging work, Store review, and a public +listing. + +**D is dominated** — strictly worse than both A and C: cert required anyway, silent install +required, and we maintain the versioned download URL. Documented in one line so it is +never re-litigated. + +--- + +## 4. ⛔ The one real blocker: 10.2.2 (dynamic code inclusion) + +**Policy 10.2.2:** + +> Your product must not attempt to fundamentally change or extend its described +> functionality or introduce features or functionality that is in violation of Store +> Policies **through any form of dynamic inclusion of code**. Your product should not, for +> example, **download a remote script and subsequently execute that script** in a manner +> that is not consistent with the described functionality. + +**What we do today:** `Services/Encoder/FfmpegLocator.cs:37-38` pins a GitHub release URL, +and `LocateAsync` downloads the zip (line 69), extracts it (line 73), and the encoder then +executes that binary. On a cold cache the app **downloads an unsigned executable from the +internet and runs it.** + +That is the pattern 10.2.2 names. The word is "script," so a narrow reviewer might quibble +about a PE binary — but 10.2.3 reinforces it, and 10.2.9 separately requires all PE files +to chain to the Microsoft Trusted Root Program, which a downloaded BtbN build does not. + +**The fix:** bundle `ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family **inside the +package** instead of downloading. No dynamic code inclusion, so 10.2.2 is satisfied. + +**This is a fix we want regardless of route.** `FfmpegLocator.cs:30-35` records that the +previous daily pin aged out of GitHub retention and **404'd on the first real recording +attempt (2026-09-01)**. Bundling kills that entire class of cold-start failure permanently, +and it also removes the startup network dependency. + +The LGPL-shared licensing reasoning in `FfmpegLocator` is unaffected — dynamic linking was +the deliberate choice for LGPL §6 compliance, not for download avoidance. Confirm +`THIRD-PARTY-NOTICES.txt` (already shipped) covers the bundled build. + +**What I got wrong initially:** I believed MSIX might forbid *launching* a bundled +executable. It does not — see §5. + +--- + +## 5. MSIX technical facts (full trust vs AppContainer) + +MSIX packages run at one of two trust levels, chosen by the manifest: + +| Trust level | Behavior | +|---|---| +| **`mediumIL` / full trust** | **Same permissions as a standard desktop app.** Package identity + some file/registry virtualization. **Can spawn child processes.** ← this is what a WPF broadcast tool wants | +| `appContainer` | Strictly isolated. Process and children see only explicitly granted resources. File/registry virtualized. | + +**A packaged WPF app declaring `runFullTrust` runs at medium integrity — it is not an +AppContainer.** Bundling and launching ffmpeg from inside the package is therefore fine. +This is the "Desktop Bridge" / `packagedClassicApp` model. + +**Critical file-write consequence:** "Writes to user profile locations (such as AppData) are +redirected to per-package locations for AppContainer apps, or **pass through for full-trust +apps**." The package install folder itself is read-only. + +**Manifest this app needs:** + +- `rescap:Capability Name="runFullTrust"` — medium integrity +- `webcam` and `microphone` capabilities +- `uap10:TrustLevel="mediumIL"` +- `uap10:RuntimeBehavior="packagedClassicApp"` + +### The full-trust recording invariant (durable architectural fact) + +`ViewModels/MainViewModel.Recording.cs` writes recordings to +`%USERPROFILE%\Downloads` or `SpecialFolder.MyVideos` (`DefaultRecordFolder()`, line +173–179), with `Microsoft.Win32.OpenFolderDialog` for overrides (`ChooseRecordFolder()`, +line 151). The whole temp-write-then-move lifecycle stays inside one directory +(line 131–132). + +**This works only because we are full trust.** At `mediumIL` those writes pass through +unvirtualized and `OpenFolderDialog` is an ordinary Win32 folder browser with no capability +token involved. **No `broadFileSystemAccess` is needed.** + +⚠️ **If anyone ever flips the manifest to `appContainer`, recording silently breaks** — +`Directory.CreateDirectory` and `File.Move` start resolving to a per-package virtualized +location. A code comment belongs at `DefaultRecordFolder()` **when the manifest lands**, not +before (a comment describing a manifest that does not exist is worse than no comment). + +--- + +## 6. Store policy verdicts — what binds, what doesn't + +Policy 7.20. "Binds" = must be satisfied to submit. + +### ⛔ Binds — blockers or real work + +| Policy | Requirement | Our status | +|---|---|---| +| **10.2.2** | No dynamic code inclusion (download-and-execute) | ❌ **VIOLATED** — see §4. Fix: bundle ffmpeg | +| **10.3.1** | Login required → supply a **working demo account** in certification notes | ☐ TODO — we require YouTube sign-in | +| **10.5.1** | **Privacy policy mandatory**; URL in Partner Center. Desktop Bridge + Win32 **always** need one. Must state what PI is accessed/collected/transmitted, how used/stored/secured, types of parties disclosed to, controls the user has, compliance with law, and be kept current | ☐ TODO — `llamachile.shop` can host it | +| **10.5.4** | Collect/store/transmit PI securely with modern cryptography | ✅ DPAPI for tokens, HTTPS to YouTube | +| **10.5.5** | No highly sensitive PI (health/**financial**) unless tied to functionality + express consent | ⚠️ **SuperChat/reward path touches financial data** — keep display-text-only processing, never log payloads | +| **10.6** | Declared capabilities must relate to the product's functions; **must not circumvent OS checks** | ✅ camera/mic for a broadcast tool is self-evident; must use standard MF/DShow/WASAPI paths | +| **10.7** | Localize for every declared language; declare **English only** | ☐ Keep the manifest to one language | +| **10.1.1** | Title must be unique, **no marketing or descriptive text, no extraneous keywords** | Store name is exactly `LlamaCasty` | +| **10.1.3** | Search terms: max 7, no pricing terms, **no other product titles** unless you published them | ☐ Cannot list `OBS` or `StreamYard` as search terms | +| **10.1.4** | Distinct informative metadata + **active Store presence** | ☐ Listing must not be a stub | +| **10.2.7** | Must enable clean uninstall | ☐ MSIX gives this free | +| **10.4.2** | Start promptly, stay responsive, shut down gracefully, survive native API exceptions | ✅ `AppLog` + dispatcher handlers exist | +| **11.11.1** | **IARC age-rating questionnaire** required | ☐ General-audience streaming tool, 12+ territory | +| **11.11.2** | Keep the rating accurate as features change | ☐ Re-visit if audience-facing features shift | +| **11.12** | **User Generated Content** — see §9 | ⚠️ Judgment call, documented not built | +| **10.8.1** | Non-game PC products **may** use a secure third-party purchase API **or** Store IAP | ✅ **Polar explicitly permitted.** Must tick the third-party purchase box in Partner Center | +| **10.8.2** | Third-party purchase: identify provider, authenticate user, obtain confirmation, PCI DSS, note in Partner Center | ☐ Covered by Polar; tick the box | +| **10.8.3** | **Individual** accounts cannot require financial info for primary functionality; requiring it forces a **Company** account | ⚠️ **Open question** — see §11 | +| **10.14** | Individual accounts usually fine for a solo dev; Company required if the product needs financial info for primary functionality, or a consumer would read the name as a business | ⚠️ Same question as 10.8.3 | +| **10.8.7** | Pricing must follow the FTC Guides Against Deceptive Pricing and must not be irrationally high vs. features | ☐ A pricing decision, not a code task | +| **10.5.8** | Must **follow** applicable safety/privacy laws re children | ☐ Follow-the-law obligation — see §8 | +| **10.1.5** | May enable acquisition of your other Store products or add-ons | N/A | + +### ✅ Does NOT bind — recorded so it is never re-raised + +| Policy | Why it doesn't apply | +|---|---| +| **10.10** (Advertising) | **Ads are injected server-side by YouTube after the RTMP handoff.** LlamaCasty displays no ads, renders no ad SDK, and controls no ad decision. Every 10.10 clause is conditioned on ads *your product displays*. Not applicable. | +| **10.2.5** (Store-only install) | 7.20 applies "installed and updated only through the Store" to **game products and Xbox console products**. A non-game PC app is not locked to MSIX. *(The 2018 v7.7 wording said "all of your apps" — that language has been narrowed.)* | +| **10.2.3** (secondary software) | ffmpeg **enhances** the product's functionality, and once bundled it is not "offered to install" as separate software | +| **10.2.8** (Windows settings) | `RegisterHotKey` and standard DShow/MF paths are supported APIs. No accessibility-API or undocumented-API tricks | +| **10.10.1 / 10.10.2** | Follows from 10.10 — no ads displayed | +| **11.13** (Storefront) | Not a storefront | +| **11.14** (Gambling) | Not applicable | +| **11.15** (Child Safety) | Prohibits products enabling exploitation/grooming/sexualization/sextortion/trafficking, and adult-themed products targeting children. A broadcast tool is not in this class. Nothing LlamaCasty does creates, uploads, or distributes child-exploiting content | +| **10.5.2** | We do not publish a customer's PI to an outside service beyond the stream they themselves broadcast | +| **10.5.7** (location) | No device location access | +| **10.2.9** | Only if we chose the Store EXE route. It would bind — see §3, route D is dominated | + +### Certification mechanics + +- Technical compliance is tested by the **Windows App Certification Kit** before submission. +- Certification takes **up to 3 business days**; the listing appears ~15 min after publish. +- MSIX package requirements: SHA2-256 block map, `StoreManifest`, 25 GB max. +- Partner Center account: individual vs company — see §11. + +--- + +## 7. MSIX disqualifiers we do NOT trip + +Verified by grep across the codebase 2026-09-27. The MSIX prep docs list four hard +disqualifiers; **all four are clear**: + +| Disqualifier | Our status | +|---|---| +| "Your application requires a Windows driver. MSIX doesn't support Windows drivers." | ✅ We **consume** DShow filters, we don't install a driver. (A creator's third-party Virtual Capture Device is their hardware, not something we ship.) | +| "Your application requires a user Windows service. MSIX doesn't support per-user Windows services." | ✅ No service installation anywhere | +| Apps requiring **elevation** for any functionality are not accepted | ✅ No `requireAdministrator` / `highestAvailable` / UAC manifest | +| "Your app's modules are loaded in-process to processes that are not in your Windows app package" (shell extensions, etc.) | ✅ No shell extensions, no in-process module loading, no jump lists | + +Also verified: there is **no `.manifest` file in the repo at all** today, so packaging is +purely additive — a new `Package.appxmanifest` plus a packaging project. The existing +unpackaged exe keeps working. + +### Other store rules worth knowing while listing + +- **10.1.4** requires an active Store presence — the listing cannot be a placeholder. +- **10.7** — declare English only, or localize the description into every declared language. +- **10.9** — the app must stay functional when notifications are disabled. Our toast stack + must not be load-bearing. + +--- + +## 8. Minors, COPPA, and the age question + +### The operator is 13+ by construction + +YouTube Terms of Service: + +- **"You must be at least 13 years old to use the Service"** +- 13–17 permitted **with parent/guardian permission** +- **Under 13 cannot create an account** — exceptions are YouTube Kids and supervised + accounts via Family Link or Google Workspace for Education +- Google Accounts: 13 is the minimum to self-manage +- **The floor varies by region** — at least one YouTube ToS variant states 14, tracking EU + digital-consent ages. Treat it as 13–14, not a fixed 13. + +**Why this does real work:** LlamaCasty *requires* a YouTube account to operate, because +streaming requires one. So the person running the broadcast is 13+ by construction. A +COPPA theory premised on "a service collecting personal information from children under 13" +has no purchase against the operator — and that is true **by construction, not by promise**. +That is the cleanest sentence available for a certification note. + +### The residual exposure + +The **viewers** may still be under 13 on supervised/family-linked accounts, so the chat +ingestion point does not fully disappear. It weakens substantially, because: + +- Live chat is restricted/disabled on supervised YouTube kid accounts +- YouTube moderates the messages before we ever see them (§9) +- Our mitigation: **retain nothing** — parse, display, discard (see `task-49`) + +### COPPA is very unlikely to bind + +COPPA applies to child-directed services and to operators collecting PI from children under +13. The three-factor test (subject matter, audience, evidence of child focus) is nowhere +near met by a broadcast tool aimed at a gaming audience. **10.5.8 is a follow-the-law +obligation, not a be-child-safe obligation** — it says "follow all applicable laws," it does +not impose COPPA duties on us directly. + +**But note the enforcement trend** (FTC, September 2025 Apitor settlement): operators are +held responsible for **what third-party components collect on their behalf**, and the FTC +expects a real inquiry into third-party collection practices. That case was about an SDK; +the principle is what matters for a chat pipe. This is a documentation obligation, and the +answer is "we integrate no third-party analytics or ad SDKs, and we persist no chat or +reward payloads." + +--- + +## 9. 11.12 User Generated Content — the judgment call + +11.12 triggers on "content that users contribute to a product and which can be viewed or +accessed by other users **in an online state**." **Chat is literally that.** If a reviewer +classifies the chat surface as UGC, the requirements are: + +1. Published **terms of service, code of conduct, and/or content guidelines** for UGC, + accessible **in-product and on your website** +2. A **means for users to report** inappropriate/illegal/harmful UGC to the developer for + review, and/or proactive detection +3. Respect **user safety, parental, and content privilege settings**, and handle restricted + access gracefully +4. **Remove or disable UGC when requested by Microsoft** +5. Disclose that content is **not created by the developer/publisher** where appropriate + +### The argument: YouTube discharges the moderation obligation upstream + +| 11.12 asks for | How YouTube already covers it | +|---|---| +| Published ToS / CoC / content guidelines | Community Guidelines, linkable from our listing | +| A reporting mechanism | Creator-side chat reporting; YouTube enforces | +| Respect parental / privilege settings | YouTube's restricted + supervised accounts | +| Remove/disable on request | YouTube removes; we never hosted the original | +| Disclose third-party UGC origin | Visibly YouTube chat; YouTube is platform of record | + +**And the enforcement is demonstrably real.** YouTube's transparency report for +**Jan–Mar 2026**: approximately **1.6 billion comments removed in a single quarter**, with +**child safety the #2 removal reason at 124,624,293** (hateful/abusive 15,977,127; +harassment/cyberbullying 71,326,861; spam/scams 1.31 B). + +**The certification-note sentence:** + +> UGC is mirrored from YouTube, which moderates it at industrial scale upstream. We render +> transiently, persist nothing, and provide no user-to-user communication surface. + +**The "persist nothing" half is ours to keep** — and it is the same property +`task-49` (profanity filter) needs. Convenient, not coincidental. + +**Our read:** 11.12 targets products hosting a UGC *community*. A chat *mirror* rendered +from YouTube — where YouTube is the platform of record and already supplies reporting, +age-gating, and parental controls — is arguably not "our" UGC. **This is a judgment call, +and we resolve it by documenting the position rather than building features on a guess.** +State the reasoning in the certification notes; Microsoft reads them. + +### The documentation we owe regardless + +| Artifact | Requirement | +|---|---| +| **Privacy policy** on `llamachile.shop` | 10.5.1. Must state: camera/mic access is user-directed and OS-gated; **no retention** of chat or reward payloads; no viewer data collected; **ffmpeg is bundled, nothing is fetched at runtime** | +| **Code of conduct + content guidelines** | 11.12 / 11.15. General audience; explicitly not directed at under-13s | +| **IARC age-rating questionnaire** | 10.11.1 | +| **Certification notes** | the 11.12 reading, the YouTube age-gate argument, the Polar third-party purchase box, the demo account | + +--- + +## 10. Camera, microphone, and "only when the user says so" + +**We do not write that certification. Windows does it — and the Store forbids us from +bypassing it.** Three layers: + +1. **Policy 10.6** — "You must not circumvent operating system checks for capability + usage." Route through the standard OS consent path; do not roll our own. +2. **Windows privacy settings** — Settings → Privacy & security → Camera / Microphone. + Since **Windows 10 1903**, *"Let desktop apps access your camera/microphone"* is the + toggle that governs a Win32 broadcast app. Turn it off and the app loses access. **That + is the "explicit user direction" mechanism, enforced by the OS, not by our EULA.** +3. **Hardware/OS indicators** — camera light (or a notification when there is no light) and + a microphone icon in the taskbar tray. Automatic. The OS is the disclosure mechanism, so + we get neither credit nor blame from it. + +### ⚠️ The open question — measure, don't assume + +Microsoft's own support page carries this caveat: + +> Desktop apps may not always appear in the list of apps available to the Camera and +> Microphone settings pages or might still be able to access your camera or microphone even +> when these settings are turned off. + +**Untested: does the Win11 desktop-app camera toggle reliably gate a DShow webcam and a +capture card?** + +- If it **does** gate them, we can make privacy-forward claims after verifying. +- If it **does not**, that is **not** a Store certification failure — we declared `webcam` + honestly and use supported APIs — but it is a real user-trust issue, and we must not + write privacy-forward marketing copy on the assumption that it works. + +**Tracked as an open item in `TASKS.md`. A guard note in `MARCOM.md` blocks privacy-forward +copy until it passes.** + +--- + +## 11. Open questions + +1. **Individual or Company Partner Center account?** (10.8.3 / 10.14.) Individual accounts + are "usually appropriate for a single developer working on their own," and a Company + account is required if the product requires financial account information **for primary + functionality**. Polar collects payment externally and we never ask the user for card + details, so we look like a clean individual case — but it is a question for Partner + Center, and getting it wrong means re-enrolling. +2. **Does the desktop-app camera toggle gate DShow?** (§10) — gates the `MARCOM.md` + privacy claim. +3. **MSIX write behavior under a real package identity** — full trust passes writes through + per the docs, but worth confirming on a real packaged build before trusting it with + recordings. +4. **Store revenue-share terms** — do not plan on any specific percentage. The terms have + moved more than once and small indie apps sometimes qualify for better rates. Verify + current terms and conditions before pricing against them. +5. **`liveBroadcasts.insert` requires `status.selfDeclaredMadeForKids`** (COPPA) — already + known, see [`research-youtube-api.md`](research-youtube-api.md) item 1. Confirm the + dialog wording is right for a tool that is never child-directed. + +--- + +## 12. Profanity filter — design constraints (feeds `task-49`) + +YouTube's Community Guidelines carry a specific **Vulgar language policy** under +"Sensitive content," alongside nudity/sexual content, child safety, and self-harm. So +profanity is a named policy surface, not an edge case. + +Three external reasons it earns a slot, plus the real one: + +1. YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has + something to attach to +2. Competitive parity — overlay tools in this lane generally offer it +3. It is a reasonable reading of 11.12's proactive-detection language +4. **The real reason: we control what gets projected on screen.** That is the creator's + call, not YouTube's. + +**Four constraints, because they protect the privacy story rather than complicate it:** + +1. **Local and on-device only.** No server, no telemetry of message content, nothing leaves + the machine. +2. **Non-persistent.** Filter at ingest, discard the raw text, keep only the display + string. Same property that makes the 11.12 answer in §9 strong. +3. **User-supplied word list**, opt-in, stored locally. **Not a hardcoded slur list baked + into the binary** — that is an unpleasant artifact, generates false positives across + dialects and languages, and becomes our problem the moment someone extracts it. A + user-curated list is also simply more useful: streamers know their community's + euphemisms and we do not. +4. **Match display text only.** Never the SuperChat amount or any reward-event field — + those are financial data under 10.5.5, and matching on them would be a real mistake. + +The existing alert box already has a ticker and three display methods +(`Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Chat.cs`), so there is probably a +**single insertion point** rather than scattered call sites. Verify before building. diff --git a/TASKS/task-48-distribution-msix.md b/TASKS/task-48-distribution-msix.md new file mode 100644 index 0000000..eb0c148 --- /dev/null +++ b/TASKS/task-48-distribution-msix.md @@ -0,0 +1,187 @@ +# TASK 48 — Distribution & packaging: route decision, MSIX, signing + +> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. +> **Research: [`TASKS/research-store-certification.md`](research-store-certification.md)** — the "why". +> Carved out of TASK 36 item 6 (release engineering) on 2026-09-27 so distribution has one +> owner instead of three scattered mentions. + +**Status:** ☐ Queued — **⏳ blocked on the creator's route decision** (item 0) + +**Goal:** get LlamaCasty in front of a user without a SmartScreen scarewall, ideally without +an annual certificate bill, and without breaking recording, capture, or audio. + +⚠️ **Nothing in this task is decided yet.** The research is done and MSIX is the +front-runner, but the route is the creator's call. Items 2–8 describe the MSIX path +*conditional on choosing it* — if the answer is Polar-hosted + a cert, most of them evaporate +and only items 0, 1, and 9 remain. + +--- + +## 0. ⛔ THE DECISION — creator, not AI + +Pick one: + +| # | Route | Cert/yr | SmartScreen | Extra work | +|---|---|---|---|---| +| **A** | Store MSIX + Store IAP | **$0** | none | MSIX packaging; Store review; **Polar deleted**; revenue cut | +| **B** | Store MSIX + **Polar** | **$0** | none | MSIX packaging; Store review; two systems to maintain | +| **C** | **Polar file hosting** + Polar + own cert | $120–300 | warning ramp | none beyond buying the cert | +| **D** | Store EXE (10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway + silent install + maintain our own URL | + +**C is the status quo already sketched in `Distribution.md:14`/`:296`** — Polar hosts the +signed exe (10GB, signed personal URLs, SHA-256). The Store is not needed for delivery at +all; its only unique value is *free Microsoft signing with no warning*. + +Cost table, full detail, and the dead-EV correction: `research-store-certification.md` §2–3. + +--- + +## 1. ⛔ Bundle ffmpeg instead of downloading it — **do this on EVERY route** + +**Not conditional on the Store. This is worth doing regardless.** + +`Services/Encoder/FfmpegLocator.cs:37-38` pins a GitHub release URL; `LocateAsync` +downloads (line 69), extracts (line 73), and the encoder executes the result. On a cold +cache the app downloads an unsigned executable from the internet and runs it. + +- **Store blocker:** policy 10.2.2 forbids dynamic code inclusion (download-and-execute). +- **Route-independent bug class:** `FfmpegLocator.cs:30-35` records that the previous + daily pin aged out of GitHub retention and **404'd on the first real recording attempt + (2026-09-01)**. Bundling kills this permanently and removes the startup network + dependency. + +**Do:** ship `ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family inside the package; +`FfmpegLocator` resolves PATH → package-local → cache, and never downloads. +`IFfmpegLocator`'s contract, the `ExtractBinaries` staging discipline, and the existing +`FfmpegLocatorTests` cold/cache/PATH coverage all still apply. + +**Licensing:** the LGPL-shared build was chosen for LGPL §6 compliance (dynamic linking = +"license text + source offer"). That reasoning is **unaffected**. Confirm +`THIRD-PARTY-NOTICES.txt` covers the bundled build. + +**Record the immutable tag** in `TASKS.md` per the licensing rule, and note the URL is now +a provenance record rather than a runtime fetch. + +--- + +## 2. ☐ `Package.appxmanifest` — full trust, camera, microphone + +Only if the Store is chosen. There is currently **no `.manifest` file in the repo at all**, +so this is purely additive. + +- `rescap:Capability Name="runFullTrust"` — medium integrity, not AppContainer +- `webcam` and `microphone` capabilities +- `uap10:TrustLevel="mediumIL"`, `uap10:RuntimeBehavior="packagedClassicApp"` +- `uap10:RuntimeBehavior="packagedClassicApp"` is what allows launching package + executables as child processes +- Declare **English only** (10.7 — otherwise the description must be localized into every + declared language) +- Block map SHA2-256, `StoreManifest`, under the 25 GB cap + +**Do NOT use `appContainer`** — see the invariant in item 6. + +## 3. ☐ Remove Velopack — 3 edit sites + +Only if the Store is chosen (the Store handles updates). Trivially removable; the code was +written defensively for exactly this. + +- `ytLive.csproj:92` — `` +- `App.xaml.cs:3` — `using Velopack;` +- `App.xaml.cs:38-46` — the sole call site, already try/caught and commented + "(non-fatal) … when no URL is set, the check is a no-op" + +**Retires:** the pending "Velopack update URL" item (`TASKS.md` open items, +`task-10-monetization.md:43`) and the self-hosted DigitalOcean droplet. + +## 4. ☐ Windows App Certification Kit + +Run before submission. Technical compliance is tested by WACK; it is not optional. Known +relevant check: the app must start promptly, stay responsive, and shut down gracefully +(10.4.2). + +## 5. ☐ Certification notes + the three documentation artifacts + +**In Partner Center (submission):** + +- ☐ **Working YouTube demo account** (10.3.1) — required, we cannot stream without sign-in +- ☐ Tick the **secure third-party purchase API** box — Polar (10.8.1 / 10.8.2) +- ☐ **The 11.12 UGC position**, stated in full — "mirrored from YouTube, which moderates it + at industrial scale upstream; we render transiently, persist nothing, and provide no + user-to-user communication surface." Full reasoning and the Q1-2026 enforcement numbers + are in `research-store-certification.md` §9 +- ☐ The **YouTube age-gate argument** — operator is 13+ by construction (§8) +- ☐ Note that the product is **general audience, not directed at under-13s** + +**On `llamachile.shop`:** + +- ☐ **Privacy policy** (10.5.1 — mandatory for Win32/Desktop Bridge). Must state: camera/mic + access is user-directed and OS-gated; **no retention** of chat or reward payloads; no + viewer data collected; **ffmpeg is bundled and nothing is fetched at runtime** +- ☐ **Code of conduct + content guidelines** (11.12 / 11.15) +- ☐ Confirm `THIRD-PARTY-NOTICES.txt` covers the bundled LGPL ffmpeg build + +**In Partner Center (listing):** + +- ☐ **IARC age-rating questionnaire** (10.11.1) — general audience, 12+ territory +- ☐ Title is exactly **`LlamaCasty`** — 10.1.1 forbids marketing text or extraneous keywords +- ☐ Search terms: max 7, no pricing terms, and **no other product titles** (10.1.3 — cannot + list `OBS` or `StreamYard`) +- ☐ Real listing content — 10.1.4 requires an active, substantive presence +- ☐ Review the **Individual vs Company** account question before enrolling (see + `research-store-certification.md` §11 item 1) — getting this wrong means re-enrolling + +## 6. ☐ The full-trust recording invariant — **comment lands WITH this work** + +`ViewModels/MainViewModel.Recording.cs:173-179` (`DefaultRecordFolder()`) writes to +`%USERPROFILE%\Downloads` or `SpecialFolder.MyVideos`; `ChooseRecordFolder()` (line 151) +uses `Microsoft.Win32.OpenFolderDialog`; the temp-write-then-move lifecycle stays in one +directory (line 131-132). + +**This works only because the package is full trust.** At `mediumIL`, user-profile writes +pass through unvirtualized and `OpenFolderDialog` is an ordinary Win32 browser with no +capability token. No `broadFileSystemAccess` needed. + +⚠️ **If anyone flips the manifest to `appContainer`, recording silently breaks** — +`Directory.CreateDirectory` and `File.Move` start resolving to a per-package virtualized +location and output vanishes. + +**Add a comment at `DefaultRecordFolder()` in this task, not before.** A comment describing +a manifest that does not exist is worse than no comment. + +**Also verify on a real packaged build** before trusting it with recordings — the docs say +full trust passes writes through, but that is worth confirming with an actual package +identity rather than an unpackaged run. + +## 7. ☐ Confirm the disqualifiers stay clear after packaging + +All four are currently clear (verified by grep 2026-09-27) — re-verify once the packaging +project exists: + +- No Windows driver installed (we consume DShow filters, we do not install one) +- No per-user Windows service +- No elevation / `requireAdministrator` / UAC manifest +- No shell extension, no in-process module loading by outside processes, no jump list + +## 8. ☐ Pre-submission gates + +- ☐ 0 warnings, full suite green +- ☐ **Vertical-recording path verified end-to-end** (compositor tier is proven by + `Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never + been exercised — `Services/Pump/FramePump.cs:645` flags off-size tiers as a known + follow-up) +- ☐ **The real-output confirmation** the creator has been doing manually +- ☐ Camera/mic/wasapi capture verified **from the packaged build**, not just unpackaged +- ☐ Clean uninstall verified (10.2.7) +- ☐ Notification-disabled path leaves the app functional (10.9) + +--- + +## Not in this task (deliberately) + +- **Velopack hardening / obfuscation / assembly splitting** — stays a GA-time decision per + TASK 36 item 6's agreed ceiling. Compile flags max at `HARDENED` + `MOCK_REWARDS`, + additive-only. +- **EULA draft/review and the THIRD-PARTY-NOTICES gate** — stay in TASK 36 item 6. +- **Vertical-canvas feature work** — the feature exists; only the *record-path test* above + is missing. +- **macOS / Avalonia port** — deferred; would be a second app, not a port. diff --git a/TASKS/task-49-chat-profanity-filter.md b/TASKS/task-49-chat-profanity-filter.md new file mode 100644 index 0000000..ca0d888 --- /dev/null +++ b/TASKS/task-49-chat-profanity-filter.md @@ -0,0 +1,94 @@ +# TASK 49 — Chat profanity filter (local, opt-in, non-persistent) + +> Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. +> **Research: [`TASKS/research-store-certification.md`](research-store-certification.md) §12.** +> Carved out 2026-09-27; shares a scope lock with nothing else. + +**Status:** ☐ Queued — not blocked (does not depend on the route decision) + +**Goal:** let the creator decide what reaches the projected chat box, without the app ever +becoming a moderator that stores what people said. + +**Size:** S. Local string work, one insertion point, tests. + +--- + +## Why + +YouTube's Community Guidelines carry a named **Vulgar language policy** under "Sensitive +content", alongside nudity, child safety, and self-harm — so profanity is a policy surface, +not an edge case. Three external reasons it earns a slot, plus the real one: + +1. YouTube treats it as policy-relevant, so 11.12's "proactive detection" language has + something to attach to +2. Competitive parity — overlay tools in this lane generally offer it +3. It is a reasonable reading of 11.12's proactive-detection language +4. **The real reason: we control what gets projected on screen.** That is the creator's + call, not YouTube's. + +--- + +## 1. ⛔ Four constraints — non-negotiable + +They exist to **protect the privacy story**, not to complicate the build: + +1. **Local and on-device only.** No server, no telemetry of message content, nothing leaves + the machine. A hosted profanity API would invert the entire 11.12/10.5.1 argument in + `research-store-certification.md` §9 — do not add one. +2. **Non-persistent.** Filter at ingest, discard the raw text, keep only the display + string. No message history, no file, no crash dump of chat payloads. This is the same + property that makes the 11.12 certification answer strong. +3. **User-supplied word list, opt-in, stored locally.** ⛔ **Not a hardcoded slur list + baked into the binary.** That is an unpleasant artifact, generates false positives + across dialects and languages, and becomes our problem the moment someone extracts it. + A user-curated list is also simply more useful — streamers know their community's + euphemisms and we do not. +4. **Match display text only.** ⛔ Never the SuperChat amount or any reward-event field. + Those are financial data under 10.5.5, and matching on them would be a real mistake. + +## 2. ☐ Verify the insertion point first + +The existing alert box already has a ticker and three display methods +(`Controls/OverlayHost.xaml`, `ViewModels/MainViewModel.Chat.cs`), so there is probably a +**single insertion point** rather than scattered call sites. + +**Do not assume.** `rg` for the chat-message ingest path and confirm the count. If it is +more than one site, say so in `HANDOFF.md` before building. + +## 3. ☐ Implementation + +- ☐ Settings toggle: **off by default** (opt-in, per constraint 3) +- ☐ Editable word list in the settings dialog, stored locally via the existing + `LayoutStore.Settings` pattern — reuse it, do not invent a second store +- ☐ Case-insensitive, **whole-word** matching with sensible boundary handling +- ☐ Smallest sensible replacement (`*` or configurable) — do not build a masking engine +- ☐ Filtered messages are **displayed** with the replacement, not dropped, so the chat does + not develop visible holes. (Dropping is a separate opt-in if we ever want it.) +- ☐ Nothing logged. The existing `AppLog` checkpoints must not capture message text +- ☐ Correctness over performance: a few hundred comparisons per message is nothing +- ☐ Non-ASCII and unicode case handling — a naive `ToLowerInvariant` will miss things +- ☐ Ticker layout must survive longer replacement strings + +## 4. ☐ Tests + +Follow the existing test conventions (`ytLive.Tests/`, `RuntimeSessionStateTests` etc.): + +- ☐ Off by default +- ☐ List matches case-insensitively +- ☐ Whole-word only — "class" must not be caught by "ass" +- ☐ Replacement renders, message not dropped +- ☐ **Amount/reward fields are never matched or masked** (constraint 4 — assert explicitly) +- ☐ Empty list behaves as "no filtering" +- ☐ Nothing persists to disk beyond the user's own list +- ☐ Unicode case folding + +--- + +## Related (not duplicated here) + +- [`task-48-distribution-msix.md`](task-48-distribution-msix.md) item 5 — the privacy + policy must state "no retention of chat or reward payloads". This task is what makes that + statement true. +- `research-store-certification.md` §9 — the 11.12 argument depends on the + persist-nothing property in constraint 2. **Do not let a future feature (chat history, + moderation logs, analytics) quietly break that assumption.** diff --git a/ai.md b/ai.md index e81a3ba..9021dea 100644 --- a/ai.md +++ b/ai.md @@ -634,6 +634,22 @@ const). Constructor-injected search dirs / tools dir / downloader (`Func>`) keep it hermetic: tests fake the network with a real in-memory zip. Constructed in the encoder step (not yet — this PR ships the seam + impl + tests only). +**⛔ The pinned-download design is now formally a defect, not a cold-start edge case (2026-09-27).** +It **failed in production on 2026-09-01** — the previous daily pin aged out of BtbN's 14-day +retention and the download 404'd on the creator's first real recording attempt. Two facts make +the "pin is a const, just bump it" assumption wrong: +- **Microsoft Store policy 10.2.2 forbids dynamic code inclusion.** Downloading an executable + from the internet and running it is the exact pattern the policy names. An MSIX submission + is rejected over it. +- Bumping the pin is whack-a-mole against a retention window we do not control. + +**Fix (TASK 48 item 1, recommended on EVERY route — it is not Store-conditional):** bundle +`ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family **inside the package**; `FfmpegLocator` +resolves PATH → package-local → cache and never downloads. This also deletes the startup +network dependency entirely. The LGPL-**shared** build choice is **unaffected** — dynamic +linking was chosen for LGPL §6 compliance ("license text + source offer"), not to enable +downloading. Analysis: `TASKS/research-store-certification.md` §4. + ### Live encoder + RTMP push (TASK 4 ship step 3 — shipped 2026-08-12, plan in TASKS.md) The encoder is a **thin orchestrator over `ffmpeg.exe`** — no H.264/AAC code in the app. It spawns the @@ -1775,3 +1791,95 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`): dedicated 8s stereo-48k ring (`AlertBufferSeconds`), drained in `FillAndMix` before the limiter and added at **unity — never ducked** (creator ruling: "don't lower my game audio for a tip"); `StartLive` clears it. Per-alert loudness = the Volume slider. + +--- + +## Windows packaging & distribution (2026-09-27 — research settled, route NOT decided) + +Full analysis: `TASKS/research-store-certification.md` (Store Policies **7.20**, effective +2026-10-22 — re-check the version before acting). Executable checklist: +`TASKS/task-48-distribution-msix.md`. **The route is the creator's decision** (Store MSIX + +Store IAP / Store MSIX + Polar / Polar-hosted + own cert / dominated Store-EXE). Do not build +packaging work before that call, and do not re-derive the options — they are tabulated in the +research file §3. + +### ⛔ Durable invariant: full trust, or recording silently breaks + +MSIX packages run at one of two trust levels. A packaged WPF app declaring +`rescap:Capability Name="runFullTrust"` runs at **mediumIL — the same permissions as a +standard desktop app, NOT AppContainer** — and may spawn child processes, which is why +bundling ffmpeg is legal. Writes to user-profile locations (AppData, Downloads, MyVideos) +**pass through unvirtualized at mediumIL**; the package install folder is read-only. + +**`ViewModels/MainViewModel.Recording.cs` writes recordings to +`%USERPROFILE%\Downloads` / `SpecialFolder.MyVideos` (`DefaultRecordFolder()`, +`ChooseRecordFolder()` via `Microsoft.Win32.OpenFolderDialog`), and the whole +temp-write-then-move lifecycle stays in one directory.** That works *only* because we are full +trust. No `broadFileSystemAccess` capability is needed, and none should be added. + +⚠️ **If anyone flips the manifest to `appContainer`, `Directory.CreateDirectory` and +`File.Move` start resolving to a per-package virtualized location and output vanishes with no +error.** A comment at `DefaultRecordFolder()` is owed **when the manifest lands** (TASK 48 +item 6) — not before; a comment describing a manifest that doesn't exist is worse than none. + +### Signing — do not buy EV + +Microsoft **removed the SmartScreen instant-bypass for EV certificates in March 2024**. An +EV-signed file now accrues reputation exactly like an OV-signed one, so EV ($400+/yr) buys +what OV (~$150–300/yr) buys. Signing is **not** instant regardless — a valid cert stops the +*malware* warning immediately, but "unrecognized publisher" clears only as SmartScreen accrues +reputation from real download volume. Cert validity is capped at **460 days** (recurring +line item) and private keys must live on an **HSM or hardware token**. The **Store MSIX route +needs no certificate at all** — Microsoft re-signs. `Distribution.md` §4.3 was corrected +2026-09-27; it previously recommended EV. + +### Distribution and licensing are independent + +A **Store-distributed** app may still verify licenses with **Polar** (policy 10.8.1 +explicitly permits a secure third-party purchase API for non-game PC products; the box is +ticked in Partner Center). So "should we use the Store?" does **not** imply "should we drop +Polar?" — the only combination that removes Polar is Store + Store IAP, and that is a +*licensing* choice which happens to ride on a *distribution* decision. (The monetisation +posture itself is unchanged and lives in the Monetization section above: free gets +everything; the branding flash is the only paid delta.) + +### ⛔ "Only when the user says so" is enforced by Windows, not by us + +Camera/mic consent is layered: Store policy **10.6** forbids circumventing OS checks; the +**Windows desktop-app camera/microphone toggle** (Settings → Privacy & security, since Win10 +1903) is the gate; hardware lights and tray indicators are the OS's disclosure mechanism. So +**route everything through the standard MF/DShow/WASAPI paths and we never write that +certification ourselves** — the OS does it and gets neither our credit nor our blame. + +⚠️ **Untested:** whether that toggle reliably gates a **DShow webcam and a capture card**. +Microsoft's own support page admits desktop apps "might still be able to access your camera +or microphone even when these settings are turned off." This is **not** a certification +failure (we declare `webcam` honestly and use supported APIs) but it is a real user-trust +issue — so **no privacy-forward marketing copy until it is measured.** Guard note added to +`MARCOM.md`. + +### ⛔ Durable invariant: chat is rendered, never stored + +`TASK 49`'s profanity filter is **local, on-device, opt-in, non-persistent**, with a +**user-supplied word list — never a hardcoded slur list baked into the binary** (unpleasant +artifact, false positives across dialects, extractable). It must **never match the SuperChat +amount or any reward field** — those are financial data under policy 10.5.5. + +The non-persistence half is not just hygiene: it is the load-bearing half of the **11.12 UGC +certification answer** (mirrored YouTube chat is moderated at industrial scale upstream; ~1.6B +comments removed in Q1 2026 alone, child safety the #2 reason at 124.6M). **No future feature +— chat history, moderation logs, analytics, crash payloads — may quietly break that +assumption.** If one is ever proposed, the 11.12 position must be re-argued first. + +### Minors + +The **operator is 13+ by construction**, not by promise: LlamaCasty requires a YouTube +account to stream, and YouTube's ToS sets the account floor at **13** (regional variants reach +14; 13–17 need parent/guardian permission; under-13s are on supervised/YouTube Kids accounts). +So a COPPA theory premised on collecting PI from under-13s has no purchase against the +operator. The residual is **viewers** — still possible on supervised family accounts — and it +is weak: live chat is restricted there, YouTube moderates before we see it, and we persist +nothing. COPPA is very unlikely to bind (not child-directed), but note the enforcement trend +(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) — +so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no +persisted chat or reward payloads."