docs: bank the Windows Store + signing research, and fix the dead EV-certificate line
The distribution answer existed only in conversation, so every session re-derived it. It is now in the map, and the route decision is explicitly parked as the creator's. new TASKS/research-store-certification.md — Store Policies 7.20 + MSIX packaging: which policies bind, which don't (and why), cert economics, camera/mic gating layers, YouTube age + COPPA, the 11.12 UGC judgment call. Two real defects surfaced, neither fixed (docs-only unit): - FfmpegLocator downloads an unsigned exe from GitHub and runs it. That is policy 10.2.2 (dynamic code inclusion) verbatim, and it is the root cause of the 2026-09-01 404 — the pin aged out of BtbN's 14-day retention on the creator's first real recording attempt. -> TASK 48 item 1, not Store-conditional. - Distribution.md:318 recommended a $400+/yr EV cert for a SmartScreen bypass Microsoft removed in March 2024. Fixed; had it shipped it would have cost $400+/yr to buy what $150 buys. Also new: TASKS/task-48 (checklist, carved out of TASK 36 item 6) and TASKS/task-49 (chat profanity filter, not blocked). ai.md gains the durable invariants — full trust or recording breaks silently, chat is rendered never stored — plus a correction to the FFmpeg locator section. MyMistakes.md records the lesson: a policy citation is a claim about scope, not just text. MARCOM.md got the privacy-copy guard but is gitignored by design, so that edit stays local and did not travel here.
This commit is contained in:
+30
-1
@@ -315,7 +315,36 @@ Single-file means everything is inside the `.exe`. No DLLs, no runtime dependenc
|
|||||||
|
|
||||||
### 4.3 Code Signing
|
### 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:**
|
**Signing process:**
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
+107
-126
@@ -1,146 +1,127 @@
|
|||||||
# HANDOFF — current state
|
# HANDOFF — current state
|
||||||
|
|
||||||
**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`.
|
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
|
||||||
**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main`
|
**Last pushed:** `938c5b3` (alert ticker). This unit (distribution/certification research) is
|
||||||
(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so.
|
**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
|
The session's open question was "how do we get this in front of users without a SmartScreen
|
||||||
compositing from **local recordings**. They assumed the live path needed a separate "redirect" —
|
scarewall" and it had been answered **in conversation only** — which meant the next session
|
||||||
**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the
|
would re-derive it. It is now in the map, and the conversation can be dropped.
|
||||||
`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".
|
|
||||||
|
|
||||||
### 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
|
### Two real defects the research surfaced (both now recorded, neither fixed — no code this unit)
|
||||||
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.
|
|
||||||
|
|
||||||
Design notes: the position rides on the frame rather than in a full-canvas frame because a
|
1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** That is Store
|
||||||
1920×1080 overlay is 8.3MB of LOH garbage per tick (~2.5GB over one 10s alert); the strip is
|
policy **10.2.2** (dynamic code inclusion) verbatim, *and* it is the root cause of the
|
||||||
~370KB. `CopyStrip` now clips **rows** to the target height — the pill rasterises at 48px, so a
|
2026-09-01 failure: the previous daily pin aged out of BtbN's 14-day retention and the
|
||||||
shorter box would have written past the buffer. **No alert box in the scene ⇒ no ticker.**
|
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
|
**The distribution route.** Research is done and MSIX is the front-runner (full trust is viable;
|
||||||
recording made **without ever going live** carried no credit — exactly the creator's complaint.
|
we trip **none** of the four MSIX disqualifiers — no driver installed, no per-user service, no
|
||||||
It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it
|
elevation, no shell extension). Four real combinations, costed in
|
||||||
runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the
|
`TASKS/research-store-certification.md` §3:
|
||||||
recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes
|
|
||||||
`BrandFlashPresenter.Enabled`.
|
|
||||||
|
|
||||||
**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`.
|
| # | Route | Cert/yr | SmartScreen | Notes |
|
||||||
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
|
| A | Store MSIX + Store IAP | **$0** | none | Polar gets deleted; revenue cut; public listing |
|
||||||
restarted. The setter now restarts the timer when re-enabling while `_running`.
|
| 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 —
|
**Nothing else was allowed to block on this** — the user is right that most of the research is
|
||||||
`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at
|
route-independent and worth banking regardless. So: research banked, dead line fixed, and only
|
||||||
1/30s like the real timer (the `Step` helpers).
|
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 two findings most likely to be forgotten
|
||||||
**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)`:
|
|
||||||
|
|
||||||
- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left
|
- **Distribution and licensing are independent.** Policy 10.8.1 lets a **Store-distributed**
|
||||||
empty. A failed delete returns `DiscardFailed` and the toast names the file + folder.
|
app verify licenses with **Polar**. So "should we use the Store?" does not imply "drop Polar?"
|
||||||
- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name.
|
Only route A removes Polar, and that is a licensing decision riding on a distribution one.
|
||||||
Blank box still means "keep the auto name" — but only on an explicit Save.
|
- **⛴ Two invariants that will break silently if touched:**
|
||||||
|
- **Full trust, or recording breaks.** `DefaultRecordFolder()` writes to Downloads/MyVideos;
|
||||||
Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the
|
that passes through unvirtualized **only** at `mediumIL`. Flip the manifest to
|
||||||
headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged".
|
`appContainer` and output vanishes with no error. A comment is owed there **when the
|
||||||
Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect).
|
manifest lands** (TASK 48 item 6) — not before.
|
||||||
|
- **Chat is rendered, never stored.** That non-persistence half is the load-bearing part of
|
||||||
## Two earlier work units (committed)
|
the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload
|
||||||
|
may be added without re-arguing 11.12 first.
|
||||||
### 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=<id>` and that process gets a private
|
|
||||||
`%APPDATA%\ytLlive\instances/<id>/` 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.
|
|
||||||
|
|
||||||
## Landmines
|
## Landmines
|
||||||
|
|
||||||
- A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027.
|
- **Stale fact inside `TASKS.md` (left alone, flagged here per the Scope Lock).** The "1.0 gates"
|
||||||
Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
section asserts "**TASK 36 is now shipped**", but the catalog row 36 reads **☐ Queued** and
|
||||||
- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second.
|
`task-36-gold-pass.md` still shows items 1–6 unchecked. The line most likely means *item 2*
|
||||||
Left alone deliberately.
|
(branding flash goes live) shipped at `9761b1d`. **Needs a one-line fix, not a code change** —
|
||||||
- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued.
|
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
|
## Next
|
||||||
|
|
||||||
1. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm
|
1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, recommended on every route, and it fixes a
|
||||||
the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027
|
real user-facing failure. *This is the next code unit.*
|
||||||
*before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2`
|
2. **The route decision** (creator) — A/B/C above, once the ffmpeg fix is out of the way.
|
||||||
+ `dotnet run --no-build` in a second shell is the known-good path.
|
3. **Vertical recording verification** — the compositor tier is proven by
|
||||||
2. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health
|
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
|
||||||
message) when a stream or recording ends.
|
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
|
||||||
3. **The creator's own eyes on the output** — the ticker-placement and brand-flash changes are proven
|
4. **Multi-instance** — route `FfmpegLocator._toolsDir` through `InstanceProfile` (same shared
|
||||||
by unit tests and the compositor, but the final proof is a local recording, since no YouTube
|
extraction race as #1, and it becomes moot if ffmpeg is bundled). Confirm the launch method:
|
||||||
private test recordings are being saved.
|
`$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path.
|
||||||
4. Push the commits when the creator asks.
|
5. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health
|
||||||
5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
|
message) when a stream or recording ends. `SessionTeardownTests` is the natural home.
|
||||||
input, Enter inserts a newline.
|
6. **`scripts/publish.sh` + Debug-only `InternalsVisibleTo` + the `LlamaCasty.exe` rename** —
|
||||||
6. Decide the alert-gating copy question above.
|
still queued from 2026-09-26; do **NOT** add `-p:PublishTrimmed=true` (WPF fails at runtime,
|
||||||
7. Ship-checklist items live in `TASKS.md` → "1.0 gates".
|
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".
|
||||||
|
|||||||
@@ -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
|
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
|
*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.
|
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.**
|
||||||
|
|||||||
@@ -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) |
|
| 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) |
|
| 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) |
|
| 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.
|
`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 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 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:
|
- **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`
|
the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish`
|
||||||
is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled).
|
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
|
**ticker** ("Funder — Super Chat · $10.00") scrolls along the very top of the frame
|
||||||
(never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings:
|
(never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings:
|
||||||
custom + fallback (not either/or), ticker on top, no ducking.
|
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.**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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` — `<PackageReference Include="Velopack" Version="1.2.0" />`
|
||||||
|
- `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.
|
||||||
@@ -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.**
|
||||||
@@ -634,6 +634,22 @@ const). Constructor-injected search dirs / tools dir / downloader (`Func<string,
|
|||||||
Task<byte[]>>`) keep it hermetic: tests fake the network with a real in-memory zip. Constructed in the
|
Task<byte[]>>`) 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).
|
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)
|
### 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
|
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
|
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
|
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.
|
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."
|
||||||
|
|||||||
Reference in New Issue
Block a user