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
|
||||
|
||||
**Certificate:** Buy an EV (Extended Validation) code-signing certificate from DigiCert or Sectigo ($200-400/year). EV certificates get instant SmartScreen reputation; DV/OV certs trigger SmartScreen warnings for months.
|
||||
> ⚠️ **The cert choice below is a live decision, and the old plan's EV recommendation is
|
||||
> dead.** Microsoft **removed the SmartScreen instant-bypass for EV certificates in March
|
||||
> 2024** — an EV-signed file now accrues reputation exactly like an OV-signed one, so paying
|
||||
> $400+/yr buys precisely what paying ~$150 buys. **Do not buy EV.**
|
||||
>
|
||||
> **But the more interesting finding is that you may not need a certificate at all.** Three
|
||||
> costs, current as of 2026-09-27:
|
||||
>
|
||||
> | Route | Cert/yr | SmartScreen | Note |
|
||||
> |---|---|---|---|
|
||||
> | **Microsoft Store (MSIX)** | **$0** — Microsoft re-signs | **No warning** | Requires MSIX packaging; Store review; public listing |
|
||||
> | **Microsoft Store (EXE/MSI)** | $120–300 — *your* cert | Warning ramp | Store does **not** re-sign; silent install mandatory. Strictly dominated — cert required anyway |
|
||||
> | **Direct + OV cert** | $150–300 | Warning ramp | Worldwide availability |
|
||||
> | **Direct + Azure Artifact Signing** | ~$9.99/mo ≈ $120 | Warning ramp | **Individuals: USA/Canada only.** Identity validation required |
|
||||
>
|
||||
> Signing does **not** clear the warning on day one either way: a valid cert stops the
|
||||
> *malware* warning immediately, but "unrecognized publisher" clears as SmartScreen accrues
|
||||
> reputation from real download volume. Budget cert validity at **460 days** (CA/B Forum
|
||||
> CSC-31) — this is a recurring line item, not a one-time purchase — and note that private
|
||||
> keys must sit on an **HSM or hardware token**.
|
||||
>
|
||||
> **The full comparison, the four real combinations, and the policy analysis live in
|
||||
> [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) §2–3.
|
||||
> The executable checklist is [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md),
|
||||
> and the route decision is the creator's.**
|
||||
|
||||
**Certificate:** Buy an **OV (Organization Validation)** code-signing certificate from DigiCert
|
||||
or Sectigo (~$150–300/year), or an **Azure Artifact Signing** profile (~$9.99/month,
|
||||
USA/Canada individuals only) if the geography allows. ⛔ **Not EV** — no SmartScreen benefit
|
||||
since March 2024. **Or buy nothing at all** if the route is the Microsoft Store.
|
||||
|
||||
**Signing process:**
|
||||
```powershell
|
||||
|
||||
+107
-126
@@ -1,146 +1,127 @@
|
||||
# HANDOFF — current state
|
||||
|
||||
**Branch:** `main` (pre-1.0, no feature branches). **Last pushed:** `7f14ffb`.
|
||||
**This session's work is COMMITTED LOCALLY, NOT PUSHED** — **four** commits ahead of `origin/main`
|
||||
(`fae8ec5`, `5aedca7`, `f5a9d46`, `de81fa3`) plus this unit. Push only when the creator says so.
|
||||
**Branch:** `main` (pre-1.0, no feature branches — creator ruling 2026-08-24).
|
||||
**Last pushed:** `938c5b3` (alert ticker). This unit (distribution/certification research) is
|
||||
**docs-only** — no code touched, no build, no tests run.
|
||||
|
||||
## The 2026-09-26 proof-of-concept round (creator reporting)
|
||||
## This unit: distribution & Store certification research is WRITTEN DOWN
|
||||
|
||||
Context: **no YouTube private test recordings are being saved**, so the creator is judging
|
||||
compositing from **local recordings**. They assumed the live path needed a separate "redirect" —
|
||||
**it does not, and that is verified, not assumed:** one `_framePump` is constructed in the
|
||||
`MainViewModel` ctor with a single `brandFlash:` callback, and both `Streaming.Operations.cs:68`
|
||||
(go-live) and `:199` (record) call that same `StartAsync`. Record+simulcast is one ffmpeg with two
|
||||
outputs. Five items, tracked in `TASKS.md` → "Creator-reported batch".
|
||||
The session's open question was "how do we get this in front of users without a SmartScreen
|
||||
scarewall" and it had been answered **in conversation only** — which meant the next session
|
||||
would re-derive it. It is now in the map, and the conversation can be dropped.
|
||||
|
||||
### Done: the alert ticker draws inside the Stream Alerts box
|
||||
| File | What it is |
|
||||
|---|---|
|
||||
| `TASKS/research-store-certification.md` | **new** — the authoritative research. Store Policies **7.20** (effective 2026-10-22, re-check the version), MSIX full-trust vs AppContainer, cert economics, a ⛔/✅ table of which policies bind and which don't, the camera/mic gating layers, the YouTube age + COPPA analysis, the 11.12 UGC judgment call, and the filter design constraints |
|
||||
| `TASKS/task-48-distribution-msix.md` | **new** — the executable checklist. Carved out of TASK 36 item 6 (code signing / installer / Velopack URL) so distribution has one owner |
|
||||
| `TASKS/task-49-chat-profanity-filter.md` | **new** — the chat filter checklist. Not blocked |
|
||||
| `Distribution.md` §4.3 | **corrected** — the EV recommendation was dead (§ below) |
|
||||
| `ai.md` | new "Windows packaging & distribution" section (durable invariants) + a correction on the FFmpeg locator |
|
||||
| `MyMistakes.md` | the policy-applicability lesson |
|
||||
| `TASKS.md` | rows 48/49, a research index, and the TASK 36 item 6 split |
|
||||
| `MARCOM.md` | ⛔ **privacy-copy guard — gitignored, so this edit is local-only and did not travel with the push** |
|
||||
|
||||
Was a **global 1920px bar pinned to the top edge**; the creator wants it "over the stream alerts
|
||||
video, not over the entire preview window". The strip is now rendered at the alert box's own size
|
||||
and the box's origin travels on the frame in a new `VideoFrame.Placement` (`(int X, int Y)?`, with
|
||||
`OriginX`/`OriginY` defaulting to 0 so full-canvas overlays are unaffected). Every blit site now
|
||||
reads `tickerFrame.OriginX/OriginY` instead of a literal `0, 0` — `SceneCompositor` ×2 plus
|
||||
`FramePump`'s static-bake `Overlay` path — and `PreviewPane.xaml`'s `AlertTickerElement` binds the
|
||||
same rect (`AlertTickerLeft/Top/Width/Height`), so preview and output can't drift.
|
||||
### Two real defects the research surfaced (both now recorded, neither fixed — no code this unit)
|
||||
|
||||
Design notes: the position rides on the frame rather than in a full-canvas frame because a
|
||||
1920×1080 overlay is 8.3MB of LOH garbage per tick (~2.5GB over one 10s alert); the strip is
|
||||
~370KB. `CopyStrip` now clips **rows** to the target height — the pill rasterises at 48px, so a
|
||||
shorter box would have written past the buffer. **No alert box in the scene ⇒ no ticker.**
|
||||
1. **⛔ `FfmpegLocator` downloads an unsigned exe from GitHub and runs it.** That is Store
|
||||
policy **10.2.2** (dynamic code inclusion) verbatim, *and* it is the root cause of the
|
||||
2026-09-01 failure: the previous daily pin aged out of BtbN's 14-day retention and the
|
||||
download **404'd on the creator's first real recording attempt**. `FfmpegLocator.cs:30-35`
|
||||
records the incident but still reads like a design choice. → **TASK 48 item 1.** Not
|
||||
Store-conditional: bundling kills this whole class of cold-start failure and deletes the
|
||||
startup network dependency. **This is the highest-value unblocked item in the repo.**
|
||||
2. **⛔ `Distribution.md:318` recommended a $400+/yr EV certificate for a benefit Microsoft
|
||||
deleted in March 2024.** Fixed. Had it shipped, it would have cost $400+/yr to buy exactly
|
||||
what $150 buys. Lesson in `MyMistakes.md` — the recurring shape is *citing a policy or a
|
||||
platform fact from memory instead of re-verifying it in the document itself.*
|
||||
|
||||
### Done: the branding flash is no longer a go-live-only behaviour
|
||||
### ⛔ The decision that is NOT made, and belongs to the creator
|
||||
|
||||
The presenter was `Start()`/`Stop()`-ed from `UpdateLiveVisuals()`'s `IsLive` branch, so a
|
||||
recording made **without ever going live** carried no credit — exactly the creator's complaint.
|
||||
It is now started **once in the `MainViewModel` ctor** and never stopped on live-state churn, so it
|
||||
runs in every scene, reaches the preview, and rides the same `FramePump` into the stream and the
|
||||
recording. The licence gate needs no live branch: `IsPremium`'s setter already pushes
|
||||
`BrandFlashPresenter.Enabled`.
|
||||
**The distribution route.** Research is done and MSIX is the front-runner (full trust is viable;
|
||||
we trip **none** of the four MSIX disqualifiers — no driver installed, no per-user service, no
|
||||
elevation, no shell extension). Four real combinations, costed in
|
||||
`TASKS/research-store-certification.md` §3:
|
||||
|
||||
**Trap that came with it (fixed):** `Enabled = false` stops the presenter's `DispatcherTimer`.
|
||||
When `Start()` was per-go-live, the next go-live restarted it; with one app-lifetime `Start()`
|
||||
nothing would, so a key entered mid-session would leave the credit dead until the process
|
||||
restarted. The setter now restarts the timer when re-enabling while `_running`.
|
||||
| # | Route | Cert/yr | SmartScreen | Notes |
|
||||
|---|---|---|---|---|
|
||||
| A | Store MSIX + Store IAP | **$0** | none | Polar gets deleted; revenue cut; public listing |
|
||||
| B | Store MSIX + **Polar** | **$0** | none | free signing **and** keep 100% — two systems to maintain |
|
||||
| C | **Polar file hosting** + own cert | $120–300 | warning ramp | **the status quo already sketched in `Distribution.md:14`/`:296`** |
|
||||
| D | Store EXE (policy 10.2.9) | $120–300 | warning ramp | **dominated** — cert anyway, silent install, own URL |
|
||||
|
||||
Test-arithmetic trap, hit twice now: `Advance(5.1)` in one call can never observe a credit —
|
||||
`Advance` opens *and* ages the presentation by the same delta, so it jumps the 2s window. Step at
|
||||
1/30s like the real timer (the `Step` helpers).
|
||||
**Nothing else was allowed to block on this** — the user is right that most of the research is
|
||||
route-independent and worth banking regardless. So: research banked, dead line fixed, and only
|
||||
the packaging work is parked.
|
||||
|
||||
### Done this unit: recording save dialog — Cancel now discards
|
||||
**Also still open, deliberately:** pricing (one-time vs one-time+monthly), the license provider,
|
||||
and therefore all licensing copy. `OverlayHost.xaml` still claims Polar unlocks alerts — wrong,
|
||||
alerts are not gated. Unresolved, queued, **not** to be papered over.
|
||||
|
||||
`RenameRecordingDialog` was always correct (Enter → `DialogResult=true`, Cancel/X/Escape → `false`).
|
||||
**The caller was not**: `FinalizeRecordingAsync` only overwrote `stem` when the dialog returned true
|
||||
and then ran `File.Move` **unconditionally**, so a cancelled dialog silently saved the recording
|
||||
under the default name. Extracted the decision into internal
|
||||
`MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved)`:
|
||||
### The two findings most likely to be forgotten
|
||||
|
||||
- `creatorSaved == false` (Cancel/Escape/X) → **delete the temp file**; the videos folder is left
|
||||
empty. A failed delete returns `DiscardFailed` and the toast names the file + folder.
|
||||
- `creatorSaved == true` (Save, or Enter on the pre-filled default) → `File.Move` to the final name.
|
||||
Blank box still means "keep the auto name" — but only on an explicit Save.
|
||||
|
||||
Test: `ytLive.Tests/RecordingSaveDialogTests.cs` (5 facts, real temp files, no WPF needed) — the
|
||||
headline asserts Cancel leaves the directory **empty**, not merely "the stem is unchanged".
|
||||
Lesson recorded in `MyMistakes.md` (falsy `ShowDialog()` falling through into a side effect).
|
||||
|
||||
## Two earlier work units (committed)
|
||||
|
||||
### 1. Branding flash is now composited into the OUTPUT (TASK 36 shipped)
|
||||
|
||||
It used to be a WPF `BrandFlashLayer` TextBlock in the preview at 25% opacity on a 300s timer — a
|
||||
credit the creator could see and **no viewer ever could**. Now one rendered `VideoFrame` goes to
|
||||
**both** the frame pump and the preview, so they cannot drift:
|
||||
|
||||
- `Services/Compositor/BrandFlashPresenter.cs` (new) — neon raster (white core + feathered red/blue
|
||||
halo, channels split opposite), 2s envelope, random on-canvas placement per presentation,
|
||||
licence gate re-checked on every read.
|
||||
- `ViewModels/MainViewModel.BrandFlash.cs` (new) — `BrandFlashFrame()` for the pump +
|
||||
`PublishBrandFlashPreview()` for the pane.
|
||||
- `FramePump` gained `brandFlash:` → the existing per-frame `flashFrame` slot, and mixes it into the
|
||||
C4 cache signature. `SceneCompositor.Overlay` is now `internal` for the shortcut path.
|
||||
- `PreviewPane.xaml`: `BrandFlashLayer` → `BrandFlashElement` (bound to the published frame).
|
||||
- `BrandFlashEnabled` is derived `!IsPremium` and no longer assignable.
|
||||
|
||||
Spec met: 500ms in / 1000ms hold / 500ms out, first credit ~5s after go-live then every
|
||||
`rand(30s)+30s`, single unwrapped line, random spot every time, fully on canvas.
|
||||
|
||||
### 2. Dev-only: two instances side by side
|
||||
|
||||
`Helpers/InstanceProfile.cs` (new). Set `YTLIVE_INSTANCE=<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.
|
||||
- **Distribution and licensing are independent.** Policy 10.8.1 lets a **Store-distributed**
|
||||
app verify licenses with **Polar**. So "should we use the Store?" does not imply "drop Polar?"
|
||||
Only route A removes Polar, and that is a licensing decision riding on a distribution one.
|
||||
- **⛴ Two invariants that will break silently if touched:**
|
||||
- **Full trust, or recording breaks.** `DefaultRecordFolder()` writes to Downloads/MyVideos;
|
||||
that passes through unvirtualized **only** at `mediumIL`. Flip the manifest to
|
||||
`appContainer` and output vanishes with no error. A comment is owed there **when the
|
||||
manifest lands** (TASK 48 item 6) — not before.
|
||||
- **Chat is rendered, never stored.** That non-persistence half is the load-bearing part of
|
||||
the 11.12 certification answer. No chat history, moderation log, analytics, or crash payload
|
||||
may be added without re-arguing 11.12 first.
|
||||
|
||||
## Landmines
|
||||
|
||||
- A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe` and broke `dotnet run` with MSB3027.
|
||||
Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
||||
- `GlobalHotkeys`: two instances registering the SAME global hotkey — Windows refuses the second.
|
||||
Left alone deliberately.
|
||||
- `OverlayHost.xaml` says Polar unlocks alerts too; alerts are not gated. Unresolved, queued.
|
||||
- **Stale fact inside `TASKS.md` (left alone, flagged here per the Scope Lock).** The "1.0 gates"
|
||||
section asserts "**TASK 36 is now shipped**", but the catalog row 36 reads **☐ Queued** and
|
||||
`task-36-gold-pass.md` still shows items 1–6 unchecked. The line most likely means *item 2*
|
||||
(branding flash goes live) shipped at `9761b1d`. **Needs a one-line fix, not a code change** —
|
||||
flagging rather than fixing because it is not this unit's job.
|
||||
- **A stale `ytLive.exe` (PID 2544) locked `bin/.../ytLive.exe`** and broke `dotnet run` with
|
||||
MSB3027. Killed. **If the build fails to copy `ytLive.exe`, check for a running instance first.**
|
||||
- **`LayerReorderPersistenceTests.RealMouseDrag_OnTheLayerList_PersistsTheReorder` is flaky by
|
||||
environment** (needs an interactive desktop session; 2 fail / 1 pass in isolation). **Run the
|
||||
suite with ytLive CLOSED.** Not a regression.
|
||||
- **`RealAppHost.RunAsync` exists for a reason** — a frame-pump test MUST `await` inside it. A
|
||||
blocking wait occupies the one shared STA thread and **hangs the whole suite with no output.**
|
||||
Always background a `vstest` run and poll the log; a foreground piped `vstest` returns nothing
|
||||
in this shell even on success.
|
||||
- **`GlobalHotkeys`: two instances registering the SAME global hotkey** — Windows refuses the
|
||||
second. Left alone deliberately.
|
||||
- **MSIX writes under a real package identity are unverified.** The docs say full trust passes
|
||||
user-profile writes through, but confirm on a real packaged build before trusting it with
|
||||
recordings (TASK 48 item 6).
|
||||
|
||||
## Test state
|
||||
|
||||
**367/367 as of `938c5b3`.** Not re-run for this unit — docs only, no code touched.
|
||||
|
||||
## Uncommitted / untracked
|
||||
|
||||
- `MARCOM.md` — the privacy-copy guard was added but the file is **gitignored by design**
|
||||
(confidential business file, `.gitignore:11`). It stays local, as intended. Same for
|
||||
`MONETIZATION.md` and `CREDENTIALS.md` — **never commit those.**
|
||||
|
||||
## Next
|
||||
|
||||
1. **Multi-instance** (#4) — route `FfmpegLocator._toolsDir` through `InstanceProfile`, and confirm
|
||||
the launch method: `dotnet run` while the first app holds `bin/…/ytLive.exe` fails with MSB3027
|
||||
*before any instance starts* (a build-output lock, not a log conflict). `$env:YTLIVE_INSTANCE=2`
|
||||
+ `dotnet run --no-build` in a second shell is the known-good path.
|
||||
2. **Post-session efficacy report** (#5) — roll up `CurrentHealth` (dropped frames, duration, health
|
||||
message) when a stream or recording ends.
|
||||
3. **The creator's own eyes on the output** — the ticker-placement and brand-flash changes are proven
|
||||
by unit tests and the compositor, but the final proof is a local recording, since no YouTube
|
||||
private test recordings are being saved.
|
||||
4. Push the commits when the creator asks.
|
||||
5. Test-console chat UX (queued): don't clear the chat window, 20px right padding on the pull-out
|
||||
input, Enter inserts a newline.
|
||||
6. Decide the alert-gating copy question above.
|
||||
7. Ship-checklist items live in `TASKS.md` → "1.0 gates".
|
||||
1. **Bundle ffmpeg (TASK 48 item 1)** — unblocked, recommended on every route, and it fixes a
|
||||
real user-facing failure. *This is the next code unit.*
|
||||
2. **The route decision** (creator) — A/B/C above, once the ffmpeg fix is out of the way.
|
||||
3. **Vertical recording verification** — the compositor tier is proven by
|
||||
`Render_VerticalTier_Outputs_1080x1920_From_The_Center_Crop`; the *record* path has never
|
||||
been run. `FramePump.cs:645` flags off-size tiers as a known follow-up.
|
||||
4. **Multi-instance** — route `FfmpegLocator._toolsDir` through `InstanceProfile` (same shared
|
||||
extraction race as #1, and it becomes moot if ffmpeg is bundled). Confirm the launch method:
|
||||
`$env:YTLIVE_INSTANCE=2` + `dotnet run --no-build` in a second shell is the known-good path.
|
||||
5. **Post-session efficacy report** — roll up `CurrentHealth` (dropped frames, duration, health
|
||||
message) when a stream or recording ends. `SessionTeardownTests` is the natural home.
|
||||
6. **`scripts/publish.sh` + Debug-only `InternalsVisibleTo` + the `LlamaCasty.exe` rename** —
|
||||
still queued from 2026-09-26; do **NOT** add `-p:PublishTrimmed=true` (WPF fails at runtime,
|
||||
not build time). See `TASKS.md` → "Shipping / release build".
|
||||
7. **The alert-gating copy** (`OverlayHost.xaml`) — fix the copy or gate the alerts; do not leave
|
||||
it lying. Blocked behind the pricing ruling.
|
||||
8. **The camera-privacy measurement** — does the Win11 desktop-app camera toggle actually gate a
|
||||
DShow webcam and a capture card? Gates all privacy-forward copy (`MARCOM.md` guard).
|
||||
9. **TASK 36:212 stale "shipped" line** — one-line fix, needs a hand.
|
||||
10. Ship-checklist items live in `TASKS.md` → "1.0 gates".
|
||||
|
||||
@@ -1094,3 +1094,40 @@ the default file name and save the file — that's what pressing enter should do
|
||||
the save option, discarding the recording." A Cancel button on a destructive-and-final dialog means
|
||||
*discard*, not *accept default*. When a dialog is the last gate before data is committed, ask what
|
||||
Cancel should destroy — don't infer "keep the default" just because the default already exists.
|
||||
|
||||
---
|
||||
|
||||
## 2026-09-27 — A policy number in my head is not a policy finding: check that the policy *applies*
|
||||
|
||||
Two confident, wrong claims in one Store-research pass, both of the same shape — I had a policy
|
||||
number and a confident paraphrase attached to it, and never asked whether the policy's own scope
|
||||
clause covered us.
|
||||
|
||||
**1. "Store policy 10.10 (Advertising) requires X"** — 10.10 does not apply to LlamaCasty at
|
||||
all. Every clause in it is conditioned on **ads your product displays**. YouTube injects ads
|
||||
*server-side after the RTMP handoff*; the app renders no ad, hosts no ad SDK, and makes no ad
|
||||
decision. I spent a cycle designing around a rule that was never in force. Same error later with
|
||||
**10.2.5** (Store-only installation): the current policy text scopes it to **game and Xbox
|
||||
console products**, and the older v7.7 wording that said "all of your apps" has been narrowed —
|
||||
citing the stale wording would have produced a confidently wrong conclusion in the *opposite*
|
||||
direction (claiming we were locked to MSIX).
|
||||
|
||||
**2. "Buy an EV certificate — EV gets instant SmartScreen reputation."** This one was worse than
|
||||
wrong, because it was already **written into `Distribution.md`** and would have cost $400+/yr.
|
||||
Microsoft **removed the SmartScreen instant-bypass for EV certs in March 2024**. EV and OV now
|
||||
accrue reputation identically. I recited a fact that had been dead for over two years, in a
|
||||
document that already contained the recommendation.
|
||||
|
||||
**The rule, and the generalisation:** *a policy citation is a claim about scope, not just about
|
||||
text.* Before acting on one, read the **applicability sentence** and answer three questions —
|
||||
does it cover our product class, does it cover our distribution model, and is the wording the
|
||||
**current** version? A policy number without a verified scope is a guess wearing a citation's
|
||||
clothes, and the failure mode is invisible because the sentence still *sounds* authoritative.
|
||||
|
||||
Corollary: **a stale external fact inside a shipped document is worse than a missing one** — it
|
||||
is trusted, acted on, and costed. When a doc makes a factual claim about the outside world
|
||||
(certificates, platform behaviour, policy text), re-verify it on touch, not on memory. This is
|
||||
the same class as the dead-Pin incident in `FfmpegLocator` (`ai.md` → FFmpeg locator): an
|
||||
external thing changed, our record of it did not, and the mismatch was discovered by a user
|
||||
rather than by a test. **Records of the outside world rot silently and are believed until they
|
||||
cost money.**
|
||||
|
||||
@@ -63,6 +63,17 @@
|
||||
| 45 | TEST-tab chat fix #2: insert body must declare `snippet.type` (400 MISSING_REQUIRED_FIELD) | ✅ Done (2026-09-25) | [`TASKS/task-45-chat-insert-type.md`](TASKS/task-45-chat-insert-type.md) |
|
||||
| 46 | Drawers: click outside the rail closes whichever is open — TEST added to the existing Stream Settings + YPP dismiss behavior | ✅ Done (2026-09-25) | [`TASKS/task-46-drawer-click-outside-close.md`](TASKS/task-46-drawer-click-outside-close.md) |
|
||||
| 47 | Alert box video: built-in/custom alert clip (+ six-animation fallback) with read-time fade in/out + ticker (now in the preview too, 3 display methods) + alert volume in the live mix | ✅ Done (2026-09-26) | [`TASKS/task-47-alert-videos.md`](TASKS/task-47-alert-videos.md) |
|
||||
| 48 | Distribution & packaging: route decision, bundled ffmpeg, MSIX + code signing | ⏳ Queued — **blocked on the creator's route decision**; bundled-ffmpeg half is unblocked and recommended | [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md) |
|
||||
| 49 | Chat profanity filter (local, opt-in, non-persistent, user word list) | ☐ Queued (2026-09-27) | [`TASKS/task-49-chat-profanity-filter.md`](TASKS/task-49-chat-profanity-filter.md) |
|
||||
|
||||
---
|
||||
|
||||
## Research index
|
||||
|
||||
| Research | Covers |
|
||||
|---|---|
|
||||
| [`TASKS/research-youtube-api.md`](TASKS/research-youtube-api.md) | YouTube Live Streaming API v3 — authoritative facts for the v3 build |
|
||||
| [`TASKS/research-store-certification.md`](TASKS/research-store-certification.md) | **Windows Store policies 7.20 + MSIX packaging + code-signing economics** (2026-09-27). Feeds TASK 48. Records which policies bind, which don't, and why — read it before re-litigating distribution |
|
||||
|
||||
---
|
||||
|
||||
@@ -104,7 +115,7 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw
|
||||
`DroppedFrames`/`StreamDuration`; `SessionTeardownTests` is the natural home for the roll-up.
|
||||
- **TASK 3** — items 16 (Text source) + 20 (RewardEvent capture → SQLite, Alerts' persistence half) open; **17 (Alerts) is DONE via TASK 43 (2026-09-24) — native six-event alert box**
|
||||
- **TASK 9** — item 5 open (webcam identity key reconciliation)
|
||||
- **TASK 10** — Velopack update URL pending
|
||||
- **TASK 10** — Velopack update URL pending → **now owned by TASK 48** (retires if the Store handles updates)
|
||||
- **Shipping / release build (creator-queued 2026-09-26)** — no publish config exists yet:
|
||||
the csproj has only `OutputType=WinExe` + `TargetFramework`, so a plain `dotnet publish`
|
||||
is **framework-dependent** (customer needs the .NET 8 Desktop Runtime preinstalled).
|
||||
@@ -198,6 +209,27 @@ second encoder path needing a "redirect". Record+simulcast is one ffmpeg with tw
|
||||
**ticker** ("Funder — Super Chat · $10.00") scrolls along the very top of the frame
|
||||
(never-baked dynamic overlay threaded through SceneCompositor + FramePump). Creator rulings:
|
||||
custom + fallback (not either/or), ticker on top, no ducking.
|
||||
- **TASK 36 item 6 (release engineering) is now TASK 48** (2026-09-27) — code signing, the
|
||||
installer, and the Velopack update URL have one owner instead of three scattered mentions.
|
||||
**EULA draft/review and the THIRD-PARTY-NOTICES license-texts gate stay in TASK 36 item 6**,
|
||||
as does the agreed build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only).
|
||||
- **TASK 48 — distribution & packaging** — **⏳ the route decision belongs to the creator.**
|
||||
Research is done and MSIX is the front-runner; the four real combinations (Store+Store IAP /
|
||||
Store+Polar / Polar-hosted+own cert / dominated Store-EXE) and their cert costs are tabulated
|
||||
in `TASKS/research-store-certification.md` §3. Two things are settled and unblocked:
|
||||
**(a) bundle ffmpeg instead of downloading it** — `FfmpegLocator` currently downloads an
|
||||
unsigned exe from GitHub and runs it, which is Store policy 10.2.2 (dynamic code inclusion)
|
||||
*and* is the root cause of the 2026-09-01 expired-pin 404; **(b) the `Distribution.md` EV
|
||||
claim is dead** — Microsoft removed the SmartScreen EV bypass in March 2024, so paying
|
||||
$400+ buys what $150 buys. Full-trust MSIX is viable (mediumIL, not AppContainer, and the
|
||||
three technical disqualifiers — drivers, per-user services, elevation — are all clear).
|
||||
Packaging, Velopack removal, WACK, the demo account, the privacy policy and the IARC
|
||||
questionnaire are all specified in the task file and waiting.
|
||||
- **TASK 49 — chat profanity filter** — queued, not blocked, size S. Local, on-device,
|
||||
**non-persistent**, opt-in, **user-supplied word list (never a hardcoded slur list)**, and it
|
||||
must **never match the SuperChat amount or reward fields** (financial data, Store 10.5.5).
|
||||
The non-persistence half is the same property that makes the 11.12 UGC certification answer
|
||||
strong, so **no future chat-history/moderation-log/analytics feature may quietly break it.**
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
encoder step (not yet — this PR ships the seam + impl + tests only).
|
||||
|
||||
**⛔ The pinned-download design is now formally a defect, not a cold-start edge case (2026-09-27).**
|
||||
It **failed in production on 2026-09-01** — the previous daily pin aged out of BtbN's 14-day
|
||||
retention and the download 404'd on the creator's first real recording attempt. Two facts make
|
||||
the "pin is a const, just bump it" assumption wrong:
|
||||
- **Microsoft Store policy 10.2.2 forbids dynamic code inclusion.** Downloading an executable
|
||||
from the internet and running it is the exact pattern the policy names. An MSIX submission
|
||||
is rejected over it.
|
||||
- Bumping the pin is whack-a-mole against a retention window we do not control.
|
||||
|
||||
**Fix (TASK 48 item 1, recommended on EVERY route — it is not Store-conditional):** bundle
|
||||
`ffmpeg.exe` + `ffprobe.exe` + the `libav*.dll` family **inside the package**; `FfmpegLocator`
|
||||
resolves PATH → package-local → cache and never downloads. This also deletes the startup
|
||||
network dependency entirely. The LGPL-**shared** build choice is **unaffected** — dynamic
|
||||
linking was chosen for LGPL §6 compliance ("license text + source offer"), not to enable
|
||||
downloading. Analysis: `TASKS/research-store-certification.md` §4.
|
||||
|
||||
### Live encoder + RTMP push (TASK 4 ship step 3 — shipped 2026-08-12, plan in TASKS.md)
|
||||
|
||||
The encoder is a **thin orchestrator over `ffmpeg.exe`** — no H.264/AAC code in the app. It spawns the
|
||||
@@ -1775,3 +1791,95 @@ published decisions (recorded in `TASKS/task-43-native-alerts.md`):
|
||||
dedicated 8s stereo-48k ring (`AlertBufferSeconds`), drained in `FillAndMix` before the
|
||||
limiter and added at **unity — never ducked** (creator ruling: "don't lower my game
|
||||
audio for a tip"); `StartLive` clears it. Per-alert loudness = the Volume slider.
|
||||
|
||||
---
|
||||
|
||||
## Windows packaging & distribution (2026-09-27 — research settled, route NOT decided)
|
||||
|
||||
Full analysis: `TASKS/research-store-certification.md` (Store Policies **7.20**, effective
|
||||
2026-10-22 — re-check the version before acting). Executable checklist:
|
||||
`TASKS/task-48-distribution-msix.md`. **The route is the creator's decision** (Store MSIX +
|
||||
Store IAP / Store MSIX + Polar / Polar-hosted + own cert / dominated Store-EXE). Do not build
|
||||
packaging work before that call, and do not re-derive the options — they are tabulated in the
|
||||
research file §3.
|
||||
|
||||
### ⛔ Durable invariant: full trust, or recording silently breaks
|
||||
|
||||
MSIX packages run at one of two trust levels. A packaged WPF app declaring
|
||||
`rescap:Capability Name="runFullTrust"` runs at **mediumIL — the same permissions as a
|
||||
standard desktop app, NOT AppContainer** — and may spawn child processes, which is why
|
||||
bundling ffmpeg is legal. Writes to user-profile locations (AppData, Downloads, MyVideos)
|
||||
**pass through unvirtualized at mediumIL**; the package install folder is read-only.
|
||||
|
||||
**`ViewModels/MainViewModel.Recording.cs` writes recordings to
|
||||
`%USERPROFILE%\Downloads` / `SpecialFolder.MyVideos` (`DefaultRecordFolder()`,
|
||||
`ChooseRecordFolder()` via `Microsoft.Win32.OpenFolderDialog`), and the whole
|
||||
temp-write-then-move lifecycle stays in one directory.** That works *only* because we are full
|
||||
trust. No `broadFileSystemAccess` capability is needed, and none should be added.
|
||||
|
||||
⚠️ **If anyone flips the manifest to `appContainer`, `Directory.CreateDirectory` and
|
||||
`File.Move` start resolving to a per-package virtualized location and output vanishes with no
|
||||
error.** A comment at `DefaultRecordFolder()` is owed **when the manifest lands** (TASK 48
|
||||
item 6) — not before; a comment describing a manifest that doesn't exist is worse than none.
|
||||
|
||||
### Signing — do not buy EV
|
||||
|
||||
Microsoft **removed the SmartScreen instant-bypass for EV certificates in March 2024**. An
|
||||
EV-signed file now accrues reputation exactly like an OV-signed one, so EV ($400+/yr) buys
|
||||
what OV (~$150–300/yr) buys. Signing is **not** instant regardless — a valid cert stops the
|
||||
*malware* warning immediately, but "unrecognized publisher" clears only as SmartScreen accrues
|
||||
reputation from real download volume. Cert validity is capped at **460 days** (recurring
|
||||
line item) and private keys must live on an **HSM or hardware token**. The **Store MSIX route
|
||||
needs no certificate at all** — Microsoft re-signs. `Distribution.md` §4.3 was corrected
|
||||
2026-09-27; it previously recommended EV.
|
||||
|
||||
### Distribution and licensing are independent
|
||||
|
||||
A **Store-distributed** app may still verify licenses with **Polar** (policy 10.8.1
|
||||
explicitly permits a secure third-party purchase API for non-game PC products; the box is
|
||||
ticked in Partner Center). So "should we use the Store?" does **not** imply "should we drop
|
||||
Polar?" — the only combination that removes Polar is Store + Store IAP, and that is a
|
||||
*licensing* choice which happens to ride on a *distribution* decision. (The monetisation
|
||||
posture itself is unchanged and lives in the Monetization section above: free gets
|
||||
everything; the branding flash is the only paid delta.)
|
||||
|
||||
### ⛔ "Only when the user says so" is enforced by Windows, not by us
|
||||
|
||||
Camera/mic consent is layered: Store policy **10.6** forbids circumventing OS checks; the
|
||||
**Windows desktop-app camera/microphone toggle** (Settings → Privacy & security, since Win10
|
||||
1903) is the gate; hardware lights and tray indicators are the OS's disclosure mechanism. So
|
||||
**route everything through the standard MF/DShow/WASAPI paths and we never write that
|
||||
certification ourselves** — the OS does it and gets neither our credit nor our blame.
|
||||
|
||||
⚠️ **Untested:** whether that toggle reliably gates a **DShow webcam and a capture card**.
|
||||
Microsoft's own support page admits desktop apps "might still be able to access your camera
|
||||
or microphone even when these settings are turned off." This is **not** a certification
|
||||
failure (we declare `webcam` honestly and use supported APIs) but it is a real user-trust
|
||||
issue — so **no privacy-forward marketing copy until it is measured.** Guard note added to
|
||||
`MARCOM.md`.
|
||||
|
||||
### ⛔ Durable invariant: chat is rendered, never stored
|
||||
|
||||
`TASK 49`'s profanity filter is **local, on-device, opt-in, non-persistent**, with a
|
||||
**user-supplied word list — never a hardcoded slur list baked into the binary** (unpleasant
|
||||
artifact, false positives across dialects, extractable). It must **never match the SuperChat
|
||||
amount or any reward field** — those are financial data under policy 10.5.5.
|
||||
|
||||
The non-persistence half is not just hygiene: it is the load-bearing half of the **11.12 UGC
|
||||
certification answer** (mirrored YouTube chat is moderated at industrial scale upstream; ~1.6B
|
||||
comments removed in Q1 2026 alone, child safety the #2 reason at 124.6M). **No future feature
|
||||
— chat history, moderation logs, analytics, crash payloads — may quietly break that
|
||||
assumption.** If one is ever proposed, the 11.12 position must be re-argued first.
|
||||
|
||||
### Minors
|
||||
|
||||
The **operator is 13+ by construction**, not by promise: LlamaCasty requires a YouTube
|
||||
account to stream, and YouTube's ToS sets the account floor at **13** (regional variants reach
|
||||
14; 13–17 need parent/guardian permission; under-13s are on supervised/YouTube Kids accounts).
|
||||
So a COPPA theory premised on collecting PI from under-13s has no purchase against the
|
||||
operator. The residual is **viewers** — still possible on supervised family accounts — and it
|
||||
is weak: live chat is restricted there, YouTube moderates before we see it, and we persist
|
||||
nothing. COPPA is very unlikely to bind (not child-directed), but note the enforcement trend
|
||||
(FTC Sept 2025 *Apitor*: operators are responsible for what third-party components collect) —
|
||||
so the answer is a documented inquiry, and ours is "no third-party analytics or ad SDKs, no
|
||||
persisted chat or reward payloads."
|
||||
|
||||
Reference in New Issue
Block a user