Four things derived this session superseded what the plan said twenty-four hours earlier,
and the docs are corrected where each one died:
**Licensing needs no server.** Polar's customer-portal endpoints authenticate on the key
itself plus the public organization_id; the org access token is only needed to create
products. Channel binding rides in `conditions: {channel_id}`, re-checked on every
validate — so the "claim page" (a hosted OAuth flow that wrote the UC id onto the key)
and the key database it implied are deleted. Device management, key rotation and
subscription auto-revocation are Polar's; four states, one lease, no callback endpoint.
**Signing is deferred until two subscribers.** It never removes the day-one SmartScreen
warning (EV's instant-bypass died in 2024), so buying it before revenue buys only the
publisher line. Renamed to Azure Artifact Signing, $9.99/mo Basic, and it requires a
paid Azure subscription. Year-one fixed cost drops from ~$135 to ~$15.
**The Store account is free.** Microsoft waived the $19 individual fee and removed the
$99 company fee in May 2026, which makes the Store the only zero-cost route to a
Microsoft-signed install — the free answer to SmartScreen, noted beside the paid one.
**Support is a filter, not a promise.** Email, best effort, no SLA, refunds on request
without argument; no training service, no helpdesk. A repeated question becomes a
training video on @llamacasty, which is content and monetization instead of a ticket.
GitHub issues on the releases repo are the deliberate friction.
Also settled: binaries live in a second Gitea repo (`llamacasty-releases`) push-mirrored
to a public GitHub repo — issues and assets public, code private, one publish direction
(a git mirror carries refs, not GitHub Release assets); `@llamacasty` is
standard-verified with intermediate features, so live streaming, >15 min uploads and
custom thumbnails work today and no SIM or ID is needed; and OAuth comes out of Testing
mode by publishing the app (100-user lifetime cap, no 7-day token death).
TASKS.md now opens with the four-day surge slice — artifact shape, output gate,
entitlement seam, real-machine day — because those are what the first public release
waits on. HANDOFF.md rewritten. MyMistakes.md gains the Polar recipe and the
"verify means three different things" entry. MARCOM.md gains the support→content loop
and the surface roles.
154 lines
9.0 KiB
Markdown
154 lines
9.0 KiB
Markdown
# Distribution & release mechanics
|
||
|
||
How the build becomes a file someone installs. **Policy — prices, entitlement, which channels
|
||
exist and why — is [`docs/ai/commercial.md`](docs/ai/commercial.md).** This file is the how.
|
||
|
||
> The previous version of this document was a 736-line Polar/MSIX/obfuscation plan. It was
|
||
> superseded on 2026-10-31; the reasoning that still matters lives in `commercial.md` and
|
||
> `docs/ai/decisions.md`, and the full text is in `git log -- Distribution.md`.
|
||
|
||
## 1. The artifact
|
||
|
||
One self-contained single-file exe plus the bundled ffmpeg folder, zipped. No .NET install on the
|
||
target machine, no installer framework, no admin rights.
|
||
|
||
```bash
|
||
dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained true \
|
||
-p:PublishSingleFile=true \
|
||
-p:IncludeNativeLibrariesForSelfExtract=true \
|
||
-p:EnableCompressionInSingleFile=true \
|
||
-p:PublishReadyToRun=true
|
||
```
|
||
|
||
- **`PublishTrimmed` stays OFF.** Trimming breaks WPF's XAML reflection (`Binding`, `DataTemplate`,
|
||
`StaticResource`). The exe is therefore fat — expect ~110–150 MB compressed with the runtime
|
||
inside, and that is the accepted cost of "copy it and run it".
|
||
- **ffmpeg stays outside the exe** — `ffmpeg\` beside it (already the app's locator contract, see
|
||
[`output.md`](output.md) → "FFmpeg locator"). Embedding it passes 200 MB and adds an extraction
|
||
step for no benefit.
|
||
- **WebView2 is the one machine dependency**: the Evergreen runtime ships with Windows 11 and most
|
||
Windows 10; a genuinely clean machine needs the bootstrapper once. Say so in the download note.
|
||
- **`AssemblyName` is still `ytLive`** → the shipped exe is `ytLive.exe` today. The rename to
|
||
`LlamaCasty.exe` is queued (three pack URIs break — see `product.md`), and until it lands every
|
||
release note names the real file.
|
||
|
||
**Portable mode.** A `portable.txt` marker beside the exe keeps the layout DB, `Assets` and
|
||
settings in `.\data\` instead of `%APPDATA%\ytLlive` (hung off `LayoutPathOverride`). Same binary,
|
||
USB-stick behaviour, nothing to uninstall.
|
||
|
||
## 2. Where releases live — the public handout repo
|
||
|
||
**The source repo stays private.** Releases live in a **second Gitea repo,
|
||
`llamacasty-releases`**, which is push-mirrored to a **public GitHub repo** (`llamacasty/releases`).
|
||
That public repo holds nothing but a README, an issue template, a known-issues page and the release
|
||
assets — so the only thing that can ever leak is content authored for publication.
|
||
|
||
Why it is shaped that way: GitHub has no "public issues on a private repo" switch, and issue
|
||
visibility follows repo visibility. An empty public repo gives a public issue tracker, a durable
|
||
binary URL for winget, and `releases/latest` for the update notice, without the code going anywhere.
|
||
|
||
**The mirror carries refs, not release assets.** Git mirrors branches and tags; GitHub Release
|
||
uploads live outside the object store, so the zip is uploaded separately.
|
||
|
||
**Setup, once:**
|
||
|
||
1. Gitea → new repo `llamacasty-releases` (private) with `README.md`,
|
||
`.github/ISSUE_TEMPLATE/bug.yml`, `known-issues.md`.
|
||
2. Gitea → that repo → **Settings → Mirror Settings → push mirror** →
|
||
`https://github.com/llamacasty/releases.git` + a GitHub PAT. One-way and force-updating, so the
|
||
public copy can never drift from what was authored.
|
||
3. Create the free GitHub **organisation** `llamacasty` so URLs aren't tied to a personal account.
|
||
4. Point the Steam store page's support URL at
|
||
`https://github.com/llamacasty/releases/issues/new/choose` — the template chooser, never the
|
||
open-issue list.
|
||
|
||
**Rejected:** a submodule (publishes a pointer, not files), `git subtree split` (three static files
|
||
don't justify the history surgery, and one stray private file in the prefix leaks forever), and
|
||
serving zips from the droplet (uses the VPS as a CDN and makes the update notice hand-rolled).
|
||
|
||
## 3. Release
|
||
|
||
```
|
||
publish → zip → SHA-256 → gh release create → winget manifest PR → update notice
|
||
```
|
||
|
||
1. **Tag and publish.** `v<major>.<minor>.<patch>` tag; upload
|
||
`LlamaCasty-<version>-win-x64.zip` with
|
||
`gh release create v1.0.0 LlamaCasty-1.0.0-win-x64.zip --repo llamacasty/releases
|
||
--notes-file RELEASE.md`. Binaries live there, never on the droplet — free CDN, durable URLs,
|
||
release notes in the same place, and `wingetcreate` reads `releases/latest` natively.
|
||
2. **Record the hash.** `sha256sum` the zip; it goes in the winget manifest and in the release
|
||
notes. This is the anti-tamper wall: the official channel delivers exactly this artifact or
|
||
fails.
|
||
3. **winget.** `wingetcreate update LlamaCasty.LlamaCasty --version <v> --urls <release-url>
|
||
--submit` — it opens the PR against `microsoft/winget-pkgs`. Manifest shape:
|
||
`InstallerType: portable`, `InstallerSha256`, `NestedInstallerType` where the zip nests the exe.
|
||
First submission is `wingetcreate new`; the identifier is `LlamaCasty.LlamaCasty`.
|
||
4. **Update notice.** Commit the current version into the releases repo (so it arrives in the same
|
||
push or a follow-up commit) or read `releases/latest` from the GitHub API. The app shows one
|
||
line — *"v1.4 is out — `winget upgrade LlamaCasty.LlamaCasty`"*. **Nothing downloads or
|
||
executes**: that is Store policy 10.2.2 and a permanent no (see `product.md`).
|
||
5. **Optional mirrors.** Scoop (a small JSON manifest hosted in a bucket) and Chocolatey (a nupkg
|
||
with a checksum) — ten minutes each, same audience.
|
||
6. **MSIX** stays buildable for the parked Store channel: `scripts/package-msix.sh` →
|
||
`artifacts/LlamaCasty.msix`, submitted nowhere until the (now free) developer account exists.
|
||
|
||
## 4. Signing — deferred until revenue
|
||
|
||
**Unsigned is the launch state.** Artifact Signing is bought at **two subscribers**, not before:
|
||
it never removes the day-one warning (EV's SmartScreen bypass died in March 2024), so the only
|
||
thing it changes early is the publisher line. Costs and posture:
|
||
[`docs/ai/commercial.md`](docs/ai/commercial.md).
|
||
|
||
- **Azure Artifact Signing** (renamed from "Trusted Signing") — **$9.99/month Basic**, 5,000
|
||
signatures, **requires a paid Azure subscription**. Individual identity validation is the slow
|
||
part (days to weeks) and must be renewed; start it when the purchase is decided.
|
||
- **⛔ No EV certificate, and no certificate with an HSM.** Microsoft removed the EV instant-bypass
|
||
in March 2024, so EV ($400+/yr) buys exactly what OV (~$150–300/yr) buys.
|
||
- **The free alternative is the Microsoft Store**: the individual developer fee was waived, and
|
||
Microsoft signs the MSIX, so a Store install never meets SmartScreen at all.
|
||
- **Unsigned costs a warning on every release**, because SmartScreen ties reputation to the file
|
||
hash. Signing accrues reputation to the **publisher** — the only exit from that loop.
|
||
- **Signing never clears the warning on day one either.** Plan for weeks.
|
||
- Secrets: the Trusted Signing profile lives in CI secrets; **never commit** a `.pfx` or
|
||
`metadata.json` with credentials. `MSIX_PFX`/`MSIX_PFX_PASSWORD` are for local test installs only.
|
||
- Verify every release: `signtool verify /pa /v <file>`, and confirm the hash in the manifest
|
||
matches the uploaded zip.
|
||
|
||
## 5. Release checklist
|
||
|
||
- Build clean, 0 warnings (`./scripts/verify.sh`), full test suite green.
|
||
- `signtool verify /pa` passes on the shipped exe.
|
||
- Launch on a clean Windows profile: first-run works, recording writes where it claims, ffmpeg is
|
||
found beside the exe, WebView2-loaded UI renders.
|
||
- Fresh install through `winget install LlamaCasty.LlamaCasty` on a second machine, then
|
||
`winget upgrade` over the previous version.
|
||
- ZIP hash published; manifest PR merged; version notice updated.
|
||
- Portable mode: `portable.txt` present → data lands in `.\data\`, deleting the folder deletes the
|
||
app's footprint.
|
||
|
||
## 6. Legal and takedown
|
||
|
||
- **`LICENSE`** (EULA) forbids redistribution, resale, reverse-engineering and circumvention, and
|
||
disclaims warranty. It is what makes a mirror site a licence breach rather than a grey area.
|
||
- **`PRIVACY.md`** must name any third party the app contacts — today that is Google/YouTube,
|
||
ffmpeg is local, and **Polar** is added the day licence validation ships.
|
||
- **`THIRD-PARTY-NOTICES.txt`** stays shipped and shown in About; the ffmpeg pin stays
|
||
`lgpl-shared`, and no GPL component may be linked (see `product.md` → "Licensing").
|
||
- **DMCA**: statutory damages need a registration ($65), and a takedown needs only the notice —
|
||
host, link, good-faith statement, signature. Worth doing once there is something to pirate.
|
||
- **What actually protects the product** is in `commercial.md` → "Anti-piracy posture": signed
|
||
hash-pinned delivery, short leases, a free tier good enough that a crack buys nothing, and a
|
||
licence ID in About. **Not obfuscation, not a native DLL, not hardware locking.**
|
||
|
||
## 7. Effort, once
|
||
|
||
| Step | Effort |
|
||
|---|---|
|
||
| Publish profile + portable mode | 0.5 day |
|
||
| Releases repo + Gitea→GitHub push mirror | 0.5 day |
|
||
| `gh release` + hash + version notice | 0.5 day |
|
||
| First winget manifest (`wingetcreate new`) | 0.5 day, then ~15 min per release |
|
||
| Clean-machine + `winget` smoke test | 0.5 day |
|
||
| Artifact Signing profile + CI step (deferred) | 0.5 day, at two subscribers |
|