Files
LlamaCasty/TASKS/task-48-distribution-msix.md
T
gramps ef60f1f3b6 TASK 48: distribution & packaging is deferred until we are near v1
Creator ruling 2026-10-26: the remaining programming work is far from
release and a complete UX redesign is about to start, so packaging,
signing, installer and updater decisions would be chasing scope that is
about to move. The task is deferred, not cancelled.

Recorded what is frozen: TASK 48 items 6-8, the signing decision, the
updater choice (Velopack's deletion left no update path, and
StorePackagingTests currently vetoes a new one), and the route question
itself - ai.md says the Store route is dead (direct + Steam) while
TASKS.md/task-48/HANDOFF still read Store MSIX + Store IAP. That
contradiction is to be settled in ONE sweep at the revisit, not patched
piecemeal now.
2026-10-26 10:20:04 -07:00

303 lines
18 KiB
Markdown

# 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:** ⏸ **DEFERRED 2026-10-26 (creator ruling) — revisit near v1, not now.** This file is the
record of what was decided and built, not an active queue: the remaining programming work is far
from release and a complete UX redesign comes first. See `TASKS.md` → Open items → the ⏸ TASK 48
bullet for exactly what is frozen (items 6-8, signing, updater, and the route contradiction against
`ai.md`). Do not start or re-litigate any of it before that revisit.
**Historical status:** 🔶 In progress — **✅ ROUTE DECIDED 2026-09-27: Microsoft Store MSIX + Store IAP.**
Polar is deleted; every licensing subsystem goes with it.
**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.
---
## 0. ✅ THE DECISION — Microsoft Store, MSIX, Store IAP (creator ruling 2026-09-27)
**Creator's stated criteria, verbatim: "zero headaches, minimal maintenance (for me) while
still providing accountability and a reasonably easy upgrade flow."**
**Ruling: distribute as an MSIX package through the Microsoft Store, and take payment through
Store IAP.** All four criteria are satisfied by the same mechanism:
| Criterion | How MSIX + Store IAP satisfies it |
|---|---|
| Zero headaches | **$0/yr** — Microsoft holds the signing certificate, the reputation, and the installer. No cert, no HSM, no annual renewal, no SmartScreen ramp |
| Minimal maintenance | Microsoft handles **updates**, **payments**, **entitlements**, **refunds**, and **customer support**. Nothing to run |
| Accountability | Microsoft reviews every submission — that *is* the accountability layer, and it is a real one |
| Easy upgrade flow | Store auto-update. **Velopack, the update URL, and the DO droplet all go** |
### The consequence that makes this cheap: the entire licensing subsystem is deleted
Choosing Store IAP over Store+Polar is what makes "minimal maintenance" real. Keeping Polar
would have left the creator maintaining a licensing backend, a customer portal, activation
limits, and an offline-grace machine — the exact burden the ruling was made to avoid.
**Dead code / deleted concepts (TASK 10 is now a teardown, not a build):**
- `Services/PolarLicenseService.cs` — HTTP validation, the swallowed network failures, the
ignored `expires_at`
- `Helpers/PolarLicense.cs` — the record type
- `ViewModels/MainViewModel.License.cs` — `PremiumUrl`, the customer portal, the
`OfflineGracePeriod = 14 days` subscription-era artifact, renewal/lapse copy
- `Controls/OverlayHost.xaml` — the incorrect "Polar unlocks alerts" copy resolves itself
- `ytLive.csproj:92` Velopack `PackageReference` + `App.xaml.cs:3,38-46` — see item 3
- The `MONETIZATION.md` provider sections (gitignored — local file)
**What replaces it:** `IsPremium` is derived from the **Store entitlement** instead of a
remote HTTP call. One bit, locally cached, refreshed by the OS. The offline-grace machine
disappears because the OS supplies license state — and with it the whole class of
"network flaky → app thinks I'm expired" bugs.
⚠️ **The watermarks posture is unchanged:** free gets everything; the branding flash stays the
ONLY paid delta (TASK 36 item 2). Store IAP changes how the bit is *obtained*, never what it
*gates*.
### Still to verify (does not block packaging work)
- ☑ **Current Store revenue-share terms — VERIFIED 15%, subscriptions included.** Read from the
ADA v8.10 PDF (downloaded and text-extracted), §6(b): 15% for Apps and their In-App Products
*not listed in* 6(b)(iii); 12% is Games-only; 30% is Xbox console / Xbox non-subscription IAP /
Windows 8. LlamaCasty is a Windows PC App, so **6(b)(i) 15% governs** ⇒ `$8.50/mo` net,
`$340` lifetime net. The agreement's own changelog (v8.0, Oct 26 2017) states it plainly:
*"implement the 85/15 revenue share for non-Game subscriptions."* Smaller-indie better rates
were **not** found in the standard terms. Microsoft also requires that any IAP products and
their pricing be declared in Partner Center.
- ☑ **Store IAP tier shape — ✅ DECIDED 2026-09-27: `$10/mo` subscription + `$400` lifetime,
no annual.** Declare **two** products in Partner Center: one auto-renewing monthly
subscription, one non-expiring lifetime IAP. ⛔ **No `.99` prices** (settled). ⛔ **No annual
product** — Microsoft does not pro-rate: *"monthly subscriptions and initial (pre-renewal)
purchases aren't eligible for a prorated refund"*, so an annual sub cancelled in year 1
forfeits the remainder **and the developer cannot refund it**. Net after the 15% Store Fee
(verified, §6(b)(i) — applies to non-Game subscriptions too): **`$8.50/mo` and `$340`**.
Note ADA §6(h): we must fulfil the subscription for the whole period, and **raising the price
disables auto-renew** — so `$10` is effectively locked for the product's life. ⛔ A discounted
subscriber→lifetime upgrade SKU is **not** approved; do not declare one.
- ☐ Individual vs Company Partner Center account (10.8.3 / 10.14 — see
`research-store-certification.md` §11 item 1). **Get this right before enrolling**; a
Store+IAP product needing financial info for primary functionality would require Company,
but a free product with a paid upgrade tier is the normal consumer shape and is fine on an
individual account.
---
## 1. ☑ Bundle ffmpeg instead of downloading it — **SHIPPED 2026-09-28**
**This is item 1 because it is genuinely first** — it fixes a user-facing failure on its own and is a hard certification gate.
`Services/Encoder/FfmpegLocator.cs` used to pin a GitHub release URL, download it on first run,
extract it, and execute the result. On a cold cache the app downloaded an unsigned executable from
the internet and ran it.
- **Store blocker:** policy 10.2.2 forbids dynamic code inclusion (download-and-execute).
- **Route-independent bug class:** 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.
**What shipped (2026-09-28, `eb0dd91` docs + this unit):**
- `scripts/fetch-ffmpeg.sh` — populates `./ffmpeg` from a pinned month-end BtbN LGPL-shared
artifact, **SHA-256 verified before extraction**, idempotent, `FFMPEG_ZIP=` override. The
binaries are **gitignored** (135 MB of DLLs would tax every commit on the 9p tree); the pinned
URL + SHA-256 are the audit trail the LGPL written-offer actually needs.
- `ytLive.csproj` — copies `ffmpeg\**` to the output with `CopyToPublishDirectory="Always"` (the
MSIX one), plus a **`GuardBundledFfmpeg` target that fails the build** if the folder is missing,
so a fresh clone finds out before a take rather than during one.
- `FfmpegLocator` — downloader, zip extraction, and the `%APPDATA%\ytLlive\tools` cache are all
**gone**. Order is now **bundle → PATH** (deliberately inverted from "the user's install wins":
a PATH ffmpeg may be a different major version or a GPL build). A missing bundle throws a loud
`IOException` naming the folder and the fetch script — the "found something on disk" path that
could mask a missing bundle is deliberately deleted.
- `ffplay.exe` (18 MB) is **not** shipped; nothing calls it.
- `THIRD-PARTY-NOTICES.txt` — "downloads" → "bundled" throughout, and the stale build tag
`autobuild-2026-08-09-13-03` (**the tag that 404'd**) corrected to `autobuild-2026-08-31-13-27`
with the exact artifact name and archive SHA-256 recorded. OpenH264 item made precise: it is
**compiled into `avcodec-63.dll`**, not a separate binary.
**Immutable tag (licensing rule):** `autobuild-2026-08-31-13-27`, artifact
`ffmpeg-N-126342-gf88b741dbf-win64-lgpl-shared.zip`, SHA-256
`46ea6521ed3877689d2ff0bd03640ac2637104415f6237d42625d13408767146`. The URL is now a **provenance
record, not a runtime fetch**. Month-end on purpose: BtbN keeps ~14 dailies but month-end builds
~2 years — a daily pin is what aged out.
**Licensing:** the LGPL-shared build was chosen for LGPL §6 compliance (dynamic linking =
"license text + source offer"). That reasoning is **unaffected**, and the notices now cover the
bundled copy explicitly.
**Tests (16 facts, `FfmpegLocatorTests`, suite 379/379):** the Good Dog integration test walks the
real build output, asserts the 9 files ship, resolves via the locator with an empty PATH, and
**executes `ffmpeg -version`** — running it is the part that matters, because a shared build only
starts if every `libav*.dll` is a sibling. Plus: bundle-over-PATH, PATH fallback, zero-byte bundle,
the loud failure, `TheBuildRefusesToCompileWithoutTheBundle`, and
`TheLocatorCarriesNoNetworkCapability` — a source-level fact that no `HttpClient`/URL/downloader
can reappear, so 10.2.2 cannot rot back.
**Answering "were there other 3p binaries needing bundling?" (verified 2026-09-28): no.** ffmpeg was
the only download-and-execute. `e_sqlite3.dll` + `WebView2Loader.dll` already ship via NuGet into
`runtimes\win-x64\native\`; OpenH264 is inside `avcodec-63.dll`; everything else is managed. The two
real gaps are **not** bundling problems: the **WebView2 Evergreen runtime** is a machine prerequisite
(item 2's manifest dependency + a graceful-failure path), and **Velopack's updater downloads and
executes** — its teardown (item 3) is a compliance action, not just cleanup.
---
## 2. ✅ `Package.appxmanifest` — full trust, camera, microphone (DONE 2026-10-22)
There was **no `.manifest` file in the repo at all**, so this was purely additive.
**Shipped:** `Packaging/Package.appxmanifest` + `Packaging/Assets/` (the four logo tiles) +
`scripts/package-msix.sh` (publish → stage → `makeappx pack`, optional `signtool` via
`MSIX_PFX`). The first real package was built on 2026-10-22: **`Package creation succeeded`,
159 MB, 509 files** in `artifacts/LlamaCasty.msix` (gitignored). The logos are generated from
the app's own artwork by `scripts/make-store-logos.py`, so listing art and app art cannot drift.
- `rescap:Capability Name="runFullTrust"` — medium integrity, not AppContainer
- `DeviceCapability` **webcam** and **microphone**
- `uap10:TrustLevel="mediumIL"` and `uap10:RuntimeBehavior="packagedClassicApp"` on
`<Application>` — the latter is what allows launching package executables (the bundled
ffmpeg) as child processes
- **English only** (`<Resource Language="en-US"/>`) — 10.7: otherwise the description must be
localized into every declared language
- Block map SHA2-256 is generated by `makeappx`; no `StoreManifest` is needed for a plain
MSIX submission; 159 MB is far under the 25 GB cap
- **x64 only** (creator ruling 2026-10-09 — `PlatformTarget=x64` already in both csproj files):
`ProcessorArchitecture="x64"`, published `-r win-x64 --self-contained` so only the win-x64
native folders ship (an RID-less build copies every RID's ONNX Runtime native, ~17 MB each)
and the package carries its own .NET runtime. Store listing system requirements:
"64-bit Windows 10 or 11".
**Do NOT use `appContainer`** — see the invariant in item 6.
**Schema gotchas found the hard way** (the packer rejects the manifest, one error at a time):
`Square310x310Logo` and `Wide310x150Logo` are **NOT** attributes of `uap:VisualElements` in the
Windows 10 schema — they belong on `<uap:DefaultTile>`, a child of `VisualElements`. And
`Assert.DoesNotContain("appContainer", manifest.ToString())` fails on the manifest's own
comment: guard the **attribute values**, not the file text.
**Guard:** `ytLive.Tests/StorePackagingTests.The_package_manifest_declares_full_trust_camera_and_microphone`
— identity (x64), full-trust application element, the three capabilities, no `appContainer`
value, en-US only, and every referenced logo exists in `Packaging/` (makeappx refuses to pack
otherwise).
## 3. ✅ Remove Velopack — 3 edit sites (DONE 2026-10-22)
The Store handles updates, so this is simply **deleted**. Trivially removable; the code was
written defensively for exactly this.
- `ytLive.csproj:92` — `<PackageReference Include="Velopack" Version="1.2.0" />` — **REMOVED**
- `App.xaml.cs:3` — `using Velopack;` — **REMOVED**
- `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") — **REMOVED**
**Guard:** `ytLive.Tests/StorePackagingTests.cs` — the ONE test asserts the app project
references no updater package (Velopack/Squirrel/NetSparkle/AutoUpdater/WinSparkle) and that no
app source file uses the updater API (matched as CODE — a `using` directive, a type usage — not
as a bare word, so a file's prose may still name what it deliberately does not do). The app was
smoke-tested after the removal: `MainWindow loaded`, no exceptions.
**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):**
- ☐ **The IARC age-rating questionnaire** (10.11.1) — general audience, 12+ territory
- ☐ **A working YouTube demo account** (10.3.1) — the app is useless to a reviewer without one.
Must be a real, maintained account able to sign in and start a stream; a dead or
credentials-lapsed account is a rejection. ⛔ **This was accidentally dropped from this list**
during the Store-IAP edit — it is in `research-store-certification.md` §4 and must be done.
- ☐ **Declare the IAP products and their pricing** in Partner Center (required for Store IAP).
⛔ **No `.99` prices** — creator ruling, and incoherent for a no-dark-patterns brand. Confirm
the **net** after the revenue share, not the list.
- ☐ Note that the product is **general audience, not directed at under-13s**
- ☐ The 11.12 UGC position is **less load-bearing now that Polar is gone** — the strong part of
the original argument was "we render transiently, persist nothing, and provide no
user-to-user communication surface," which is *unaffected*. State it in full anyway; the
reasoning and the Q1-2026 YouTube enforcement numbers are in
`research-store-certification.md` §9
- ☐ The **YouTube age-gate argument** — the operator is 13+ by construction (§8)
**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):**
- ☐ 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
## 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.