diff --git a/Distribution.md b/Distribution.md index 5f56951..43ef9f9 100644 --- a/Distribution.md +++ b/Distribution.md @@ -1,736 +1,120 @@ -# Distribution & IP Protection Plan +# Distribution & release mechanics -> 360-degree protection strategy for LlamaCasty (.NET 8 / WPF). +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. -> ## ⛔ SUPERSEDED IN PART — 2026-09-27 -> -> **Distribution is now the Microsoft Store: MSIX package + Store IAP.** The creator's criteria -> were *"zero headaches, minimal maintenance (for me) while still providing accountability and -> a reasonably easy upgrade flow"*, and that ruling deleted this document's central premise. -> -> **What still stands below:** §1 (code architecture / IP protection), §5 (obfuscation posture), -> §6 (legal), §7 (hardening) — those are about protecting the *binary*, and an MSIX package is -> still a binary. **§1.2's build-posture ceiling (HARDENED + MOCK_REWARDS, additive-only) -> remains the agreed ceiling.** -> -> **What is dead:** -> - **Polar as the distribution backbone** — the entire section below. Store IAP handles -> checkout, entitlements, refunds, tax, and the customer portal. So does **file hosting**: -> the Store is the delivery mechanism, so "upload `LlamaCasty.exe` to Polar as a File -> Download benefit" (§2) no longer exists as a step. -> - **Code signing as our problem** — MSIX is signed by **Microsoft**. There is no certificate -> to buy, no HSM, no annual renewal. See §4.3, which was already corrected to say this, and -> `TASKS/research-store-certification.md` §2–3. -> - **Velopack / the update URL / the DO droplet** — Store auto-update. -> -> **The authoritative plan is now [`TASKS/task-48-distribution-msix.md`](TASKS/task-48-distribution-msix.md).** -> Keep this file for the protection sections; do not re-derive a delivery strategy from it. +> 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 -## Polar.sh as the Distribution Backbone — ⛔ OBSOLETE (2026-09-27) +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. -> **Historical record only.** Retained because the fee tables and the reasoning about *why* -> one-time beat subscription were worth working out. **None of the "How We Use It" rows are -> current**, with one exception worth keeping: the Merchant-of-Record point — *someone else -> collects and remits VAT/GST* — is still true under Store IAP, because **Microsoft** is now -> that someone. That fact alone was the strongest argument for the ruling. - -Polar handled everything between "customer wants to pay" and "customer has a working license key." We didn't build any of that. What Polar *was* going to give us: - -| Capability | How We Use It | -|------------|---------------| -| **License key management** | Polar generates, validates, expires, rotates, and revokes keys. Activation limits (device caps) are built-in — no custom device registry. | -| **File hosting** (up to 10GB) | Upload `LlamaCasty.exe` (self-contained, ~80-120MB) as a "File Download" benefit. Customers get signed, personal download URLs. SHA-256 checksums included. | -| **Checkout** | Polar's hosted checkout page handles payment. No Stripe integration, no PCI scope, no custom checkout UI. | -| **Customer portal** | Customers self-serve: view/copy license keys, see expiry, deactivate devices, rotate compromised keys. We don't build a portal. | -| **One-time orders** | **Chosen model (2026-09-21):** perpetual license via a one-time Polar order — no subscription. Polar also supports recurring billing if ever needed, but we do not use it. | -| **Merchant of Record** | Polar collects and remits VAT/GST/sales tax globally. We never touch tax compliance. | -| **Webhooks** | Polar notifies our backend on purchase, cancellation, key rotation. Used for optional telemetry (section 6.3). | - -**Polar pricing (2026):** 5% + $0.50 per transaction (Starter plan). No monthly fee. ⛔ *No -longer paid to anyone — the equivalent cost is now inside the Store revenue share, whose rate -is unverified (see `TASKS/research-store-certification.md` §11).* - -**What Polar did NOT handle:** Obfuscation, code signing, anti-tamper, runtime protection, EULA, DMCA. That's all us — sections 1-3, 5-7 below. - ---- - -## 1. Code Architecture for Protection - -### 1.1 Assembly Splitting (est. 2–3 days) - -Split the monolith into three assemblies with different protection tiers: - -| Assembly | Contents | Protection Level | -|----------|----------|-----------------| -| `LlamaCasty.Core.dll` | Models, DTOs, interfaces, enums, constants | Light obfuscation (symbol renaming only) | -| `LlamaCasty.Services.dll` | YouTube auth, stream service, encoder, audio pipeline, layout store | Full obfuscation + control flow + string encryption | -| `LlamaCasty.exe` | WPF UI, ViewModels, XAML | No obfuscation (XAML constraints) | - -**Why:** Obfuscators break WPF XAML/BAML bindings when they rename types referenced in `DataContext`, `Binding Path=`, or `x:Name`. Keeping the UI assembly separate lets you protect the business logic aggressively while leaving the presentation layer safe. - -**How:** -- `LlamaCasty.Core` → Class library, `net8.0`, no WPF dependency. Pure C#. -- `LlamaCasty.Services` → Class library, `net8.0`, references Core. No XAML. -- `LlamaCasty` → WPF app, references both. All `MainWindow.xaml`, `Themes/`, ViewModels live here. - -### 1.2 Obfuscation-Friendly Patterns (est. 1–2 days, ongoing) - -**Do:** -- Use `public`/`internal` properties with `{ get; set; }` for data binding (XAML `Binding Path=StringLiteral` resolves at runtime by string match — obfuscators handle this if configured). -- Use `[Obfuscation(Exclude = true)]` on anything bound to XAML via `Binding`, `CommandParameter`, or `x:Name`. -- Prefer `nameof()` for property names passed to `INotifyPropertyChanged` so renaming works. -- Use `RelayCommand` (already in use) — it takes lambdas, not string names. - -**Avoid:** -- `FindName("SomeControl")` in code-behind (breaks if XAML name is mangled). -- `x:Name` references accessed from code-behind (use `Binding` instead). -- `Activator.CreateInstance(Type.GetType("SomeString"))` — obfuscator renames the type string. -- `Assembly.Load("LlamaCasty.Core")` by string — use direct references. -- `[Serializable]` DTOs that get binary-serialized (renamed fields break deserialization). - -### 1.3 Strategic Attribute Placement (est. 1 day) - -```csharp -// Exclude from obfuscation — XAML binding relies on property name -[Obfuscation(Exclude = true)] -public string RecordFolderDisplay { get; } - -// Exclude — DTO serialized to SQLite -[Obfuscation(Exclude = true)] -public class StreamHealth { ... } - -// Exclude — ViewModels bound by name in XAML -[Obfuscation(Exclude = true)] -public partial class MainViewModel : ViewModelBase { ... } - -// Safe to obfuscate — internal service, no XAML binding -public class FfmpegEncoder { ... } +```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 ``` -**Rules:** -- All ViewModels: `[Obfuscation(Exclude = true)]` (XAML `Binding Path=` resolves by name at runtime). -- All Models/DTOs used in SQLite: `[Obfuscation(Exclude = true)]` (column names are strings). -- All public service interfaces: `[Obfuscation(Exclude = true)]` (DI resolution). -- Internal service implementations: safe to obfuscate. -- Helpers/Converters in XAML: `[Obfuscation(Exclude = true)]` on the class. +- **`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. Build Pipeline +## 2. Release -### 2.1 Build Configuration (est. 0.5 day) - -In `ytLive.csproj`: - -```xml - - net8.0-windows10.0.19041.0 - WinExe - true - true - win-x64 - true - true - true - partial - LlamaCasty - 0.9.0 - LlamaCasty - LlamaCasty - +``` +publish → zip → SHA-256 → GitHub Release → winget manifest PR → update notice ``` -**Key decisions:** -- `PublishTrimmed` + `TrimMode=partial`: Trims unused BCL code but preserves WPF infrastructure. `full` trimming breaks `Binding`, `DataTemplate`, and XAML reflection. -- `PublishSingleFile`: One `.exe` to distribute. Self-contained = no .NET runtime dependency on user machines. -- `win-x64`: Target the dominant platform. ARM64 is a separate RID if needed later. +1. **Tag and publish.** `v..` tag; attach `LlamaCasty--win-x64.zip` + to the **GitHub Release**. 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 --urls + --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.** Write the current version into a small version file on the site (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 Individual account exists. -### 2.2 Obfuscator Integration (est. 2–3 days) +## 3. Signing -**Recommended: Eazfuscator.NET** (free for projects under $1M revenue, excellent WPF support, .NET 8 compatible). - -Alternative commercial: **ArmDot** ($399/yr, better control flow, built-in anti-tamper). - -**Integration as post-publish step:** +**Azure Trusted Signing, ~$9.99/month** — Microsoft-issued, individuals eligible after identity +verification, and it accrues SmartScreen reputation without buying a certificate. ```powershell -# publish.ps1 — called after dotnet publish -param([string]$PublishDir = "publish") - -dotnet publish ytLive.csproj -c Release -r win-x64 ` - --self-contained true ` - -p:PublishSingleFile=true ` - -p:PublishTrimmed=true ` - -p:TrimMode=partial ` - -o $PublishDir - -# Obfuscate the Services assembly (core business logic) -eazfuscator.exe "$PublishDir\LlamaCasty.Services.dll" ` - --target-framework net8.0 ` - --Renaming-mode unprintable ` - --Control-flow-obfuscation true ` - --String-encryption true ` - --Resource-encryption true - -# Obfuscate the Core assembly (lighter touch) -eazfuscator.exe "$PublishDir\LlamaCasty.Core.dll" ` - --target-framework net8.0 ` - --Renaming-mode unprintable - -# Do NOT obfuscate LlamaCasty.exe (WPF entry point + XAML) +signtool sign /v /fd SHA256 /tr http://timestamp.acs.microsoft.com /td SHA256 ` + /dlib "Azure.CodeSigning.Dlib.dll" /dmdf "metadata.json" LlamaCasty.exe ``` -### 2.3 XAML/BAML Handling (est. 1 day research) - -**Problem:** WPF compiles XAML into BAML (binary XAML) embedded as resources. Obfuscators that rename types break BAML because it references types by assembly-qualified name. - -**Solution:** -- Do NOT obfuscate the main `.exe` assembly (contains all BAML). -- Only obfuscate `Core` and `Services` DLLs (no XAML). -- If an obfuscator must touch the exe: use Eazfuscator.NET's `[Obfuscation(Exclude = true, Feature = "XAML")]` attribute on types referenced in BAML, or configure the obfuscator to skip the `LlamaCasty.g.resources` manifest resource. - -**Testing:** After obfuscation, launch the app and verify: -- All screens render correctly -- `Binding` expressions resolve (no blank/missing data) -- `DataContext` inheritance works -- Storyboards/animations play -- `ContextMenu` items bind correctly -- Dialogs open/close properly - -### 2.4 CI/CD Pipeline (est. 1 day) - -```yaml -# .github/workflows/release.yml -steps: - - uses: actions/checkout@v4 - - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 8.0.x - - - name: Test - run: dotnet test --no-restore -c Release - - - name: Publish - run: dotnet publish ytLive.csproj -c Release -r win-x64 --self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true -p:TrimMode=partial -o publish - - - name: Obfuscate - run: | - eazfuscator.exe publish/LlamaCasty.Services.dll --target-framework net8.0 --Renaming-mode unprintable --Control-flow-obfuscation true --String-encryption true - eazfuscator.exe publish/LlamaCasty.Core.dll --target-framework net8.0 --Renaming-mode unprintable - - - name: Sign - run: | - $cert = Get-Content -Raw -Path secrets/certificate.pfx | ConvertTo-SecureString -AsPlainText -Force - Set-AuthenticodeSignature -FilePath publish/LlamaCasty.exe -Certificate $cert -TimestampServer http://timestamp.digicert.com -HashAlgorithm SHA256 - - - name: Upload to Polar - run: | - # Upload the signed .exe to Polar as a file download benefit - # Update the product's file benefit with the new version - curl -X POST "https://api.polar.sh/v1/products/{product_id}/benefits" \ - -H "Authorization: Bearer $POLAR_ACCESS_TOKEN" \ - -F "type=file_download" \ - -F "file=@publish/LlamaCasty.exe" -``` - ---- - -## 3. Obfuscation Strategy - -### 3.1 Recommended Obfuscator - -**Eazfuscator.NET** (primary): -- Free for projects under $1M revenue — fits solo dev. -- Excellent WPF support: knows to skip BAML resources, handles `INotifyPropertyChanged` patterns. -- .NET 8 support confirmed. -- Symbol renaming (unprintable Unicode), control flow obfuscation, string encryption, resource encryption. -- Simple CLI integration. - -**ArmDot** (commercial, $399/yr): -- Better control flow (virtualization of critical methods). -- Anti-tamper and anti-debug built in. -- Better at handling complex WPF scenarios. - -**What NOT to use:** -- ConfuserEx — abandoned, doesn't support .NET 8. -- Obfuscar — limited WPF support, BAML breakage issues. -- Dotfuscator Community — too basic, no string encryption. - -### 3.2 What to Obfuscate - -| Technique | Where | Effect | -|-----------|-------|--------| -| Symbol renaming (unprintable) | `Services.dll`, `Core.dll` | All type/method/field names → Unicode garbage. 10x decompilation difficulty. | -| Control flow obfuscation | `Services.dll` | Flattens `if/else/switch` into state machines. Decompiler produces spaghetti. | -| String encryption | `Services.dll` | All string literals encrypted; decrypted at runtime. Hides API URLs, error messages, config values. | -| Resource encryption | `Services.dll` | Embedded resources encrypted. Prevents extraction of embedded data. | -| Anti-tamper | `Services.dll` | CRC check on method bodies at runtime. Detects binary patching. | - -### 3.3 What to Exclude - -| Item | Reason | -|------|--------| -| `LlamaCasty.exe` (WPF entry point) | BAML resources break if types are renamed. | -| All ViewModels | `Binding Path=` resolves property names at runtime. | -| All Models/DTOs | SQLite column mapping uses property names as strings. | -| `LayoutStore` (public interface) | Called by ViewModels via interface — renaming breaks DI. | -| All `IValueConverter` implementations | XAML references converters by type name in `StaticResource`. | -| All `MarkupExtension` subclasses | XAML parser resolves by type name. | -| `App.xaml.cs` entry point | Must be discoverable. | - -### 3.4 Testing Obfuscated Build (est. 1 day per release) - -**Automated checks:** -1. App launches without crash (startup log at `%APPDATA%\ytLlive\startup.log`). -2. All 5 scenes load (Starting/Live/BRB/Chat/Ending). -3. Social bar renders with entries. -4. TRAX button click opens file picker. -5. Recording starts/stops correctly. -6. YouTube OAuth flow completes (if signed in). -7. Settings panel opens from Gear menu. -8. All 247 unit tests pass against the unobfuscated assemblies (tests never run against obfuscated code). - -**Manual smoke test:** -- Start a recording, verify file appears in configured folder. -- Open social dialog, add/edit/delete a handle. -- Toggle mic mute, verify meter responds. -- Switch scenes via thumbnail strip. - ---- - -## 4. Distribution Artifacts - -### 4.1 Files to Ship - -``` -LlamaCasty.msix (full-trust x64 package, self-contained, ~159MB) -``` - -Built by **`scripts/package-msix.sh`** (TASK 48 item 2, shipped 2026-10-22): it publishes -`Release -r win-x64 --self-contained` into `artifacts/msix-stage`, stages -`Packaging/Package.appxmanifest` (as `AppxManifest.xml`) + `Packaging/Assets/`, and packs with -the Windows SDK's `makeappx` → `artifacts/LlamaCasty.msix` (gitignored). Unsigned by default — -**the Store signs submissions**; set `MSIX_PFX`/`MSIX_PFX_PASSWORD` to sign a package for a -local test install. The manifest is a full-trust `packagedClassicApp` at `mediumIL` with -`runFullTrust` + webcam + microphone (see `TASKS/task-48-distribution-msix.md` item 2). - -A single-file `.exe` is **no longer the artifact**: the Store needs a package, and the package -carries the runtime (~80-120MB of it) plus the bundled ffmpeg (~135MB of DLLs). No installer -framework and no admin rights — the Store handles install, update and uninstall. - -### 4.2 Distribution via Polar — ⛔ OBSOLETE (2026-09-27) - -> **Superseded by Microsoft Store MSIX + Store IAP** (ruling in -> `TASKS/task-48-distribution-msix.md` item 0). The Store re-signs, hosts, updates, and takes -> payment — so file hosting, license keys, checkout, the customer portal, device deactivation and -> key rotation are **all** handled by Microsoft and none of it needs building. Retained below as a -> record of the model that was designed and abandoned. ⚠️ **The `$29`/`$49` one-time prices in this -> section are dead** — the settled pricing is **`$10/mo` or `$400` lifetime**, net of a verified -> 15% Store share. - -**Product setup on Polar (as designed, never shipped):** - -1. Create a product: "LlamaCasty" — **one-time purchase** (perpetual license; $29 founder / $49 list). -2. Add two benefits: - - **License Key** — brandable prefix `LLAMA-****`, activation limit (e.g., 3 devices), optional expiry. - - **File Download** — upload the signed `LlamaCasty.exe` (~80-120MB). Polar generates signed, personal download URLs. SHA-256 checksum included. - -**Customer flow:** -1. Customer visits `buy.polar.sh/polar_cl_...` (the Premium URL already in `MainViewModel.PremiumUrl`). -2. Customer pays via Polar checkout. -3. Polar generates a license key + provides a signed download URL. -4. Customer downloads `LlamaCasty.exe`, runs it, enters license key in Settings. -5. App calls `PolarLicenseService.ValidateAsync(key)` → Polar API validates → `IsValid = true`. -6. Premium features unlock locally (persisted in `LayoutStore.Settings`). - -**Activation limits:** -- Polar supports `limit_activations` per license key (e.g., 3 devices). -- Client calls `/v1/customer-portal/license-keys/activate` before first validation. -- Polar tracks activations; customer can deactivate via Polar's customer portal. -- No custom device registry needed — Polar handles it. - -**Key rotation:** -- If a key is compromised, customer or merchant can rotate it via Polar. -- Previous key stops validating immediately. Status, usage, limits, expiry preserved. - -**What the Store does instead:** the customer installs from the Store listing, pays through Store -checkout, and the app reads the entitlement the OS already holds. There is no key to enter, no -activation call, no portal to visit, and no offline-grace period to maintain — which also retires -the whole "network flaky → app thinks I'm expired" bug class in §4.5. - -### 4.3 Code Signing - -> ⚠️ **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 -$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2( - "path\to\certificate.pfx", - "password", - [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable -) -Set-AuthenticodeSignature -FilePath "LlamaCasty.exe" ` - -Certificate $cert ` - -TimestampServer "http://timestamp.digicert.com" ` - -HashAlgorithm SHA256 -``` - -**Storage:** Certificate `.pfx` file stored in GitHub Secrets (or Azure DevOps secure files). Never committed to the repo. - -### 4.4 Update Mechanism — ⛔ OBSOLETE (2026-09-27) - -> **Superseded by Store updates.** Microsoft delivers and updates the app from the listing, so -> there is nothing to check and no manifest to host. The Velopack updater that used to sit in -> `App.xaml.cs` **downloaded *and executed***, which Store policy **10.2.2** does not permit — its -> removal was a **compliance action**, not optional cleanup, and it is **done** (TASK 48 slice 3, -> 2026-10-22: the package reference and the bootstrap are gone; `ytLive.Tests/StorePackagingTests.cs` -> guards it). The Polar-native design below is retained as a record of the approach that was -> rejected; it is not an option. - -**Polar-native approach (no custom infrastructure):** - -```csharp -// In Services.dll (obfuscated) -public class UpdateCheckService -{ - // Polar's customer portal API — we check if a newer file is available - // by validating the license key and checking the benefit's file checksum - private const string ValidateUrl = "https://api.polar.sh/v1/customer-portal/license-keys/validate"; - - public async Task CheckForUpdateAsync(string currentVersion, string licenseKey) - { - // Validate the key — Polar returns the current benefit state - // If the file benefit's checksum changed, a new version is available - // The download URL is Polar's signed URL — we never hardcode it - // ... implementation in PolarLicenseService ... - } -} -``` - -**Design:** -- App validates license key on startup (already implemented in `PolarLicenseService`). -- Polar's validation response includes benefit metadata — if the file benefit's SHA-256 changed, a new version exists. -- App prompts user to download from Polar's customer portal (or provides the signed URL). -- No custom update manifest, no self-hosted endpoints, no signing key infrastructure. -- Version obsolescence: Polar can disable old file benefits, making old versions unsupported. - -**Alternative (if Polar's API doesn't expose file version metadata):** -- Host a lightweight `version.json` on GitHub Releases (public, no secrets). -- App checks GitHub Releases API for latest version. -- Download link points to Polar's customer portal (not direct download). -- This adds one external dependency (GitHub) but keeps Polar as the source of truth. - -### 4.5 Licensing Logic — ⛔ BEING DELETED (2026-09-27) - -> ⚠️ **"Already implemented" was true as of 2026-09-21 and is no longer.** `PolarLicenseService` -> and `Helpers/PolarLicense.cs` are being **removed** as part of the TASK 10 teardown (TASK 48 item 0). -> Store IAP replaces the whole validation hop: `IsPremium` is derived from the **OS-supplied Store -> entitlement**, so there is no network call, no key, and — importantly — **no 14-day offline-grace -> machine to protect**, which retires the entire "network flaky → app thinks I'm expired" bug class. -> What survives is only `IsPremium` → `BrandFlashEnabled = !value`. Retained below as the record of -> what shipped and why it no longer applies. - -```csharp -// Services/PolarLicenseService.cs — shipped under Polar; being deleted -// Calls Polar's customer-portal validation endpoint (no auth required) -// Survives obfuscation — property names excluded via [Obfuscation(Exclude = true)] -[Obfuscation(Exclude = true)] -public class LicenseValidationResult -{ - public bool IsValid { get; set; } - public string? Status { get; set; } // "granted", "revoked", "disabled" - public DateTime? ExpiresAt { get; set; } - public string? Error { get; set; } -} -``` - -**What shipped under Polar (now being deleted):** -- `PolarLicenseService` validated keys against the Polar API. -- `OrganizationId` was hardcoded. -- License state persisted in `LayoutStore.Settings` (SQLite) with an offline grace period. -- `PremiumUrl` pointed to Polar checkout. - -**What survives obfuscation:** The validation logic in `Services.dll` is obfuscated. An attacker who decompiles the exe sees only the UI calling an interface — the actual Polar API call is in the obfuscated DLL. - ---- - -## 5. Runtime Protection - -### 5.1 Anti-Debugging (est. 1–2 days) - -```csharp -// In Services.dll (obfuscated) -internal static class AntiTamper -{ - [System.Runtime.InteropServices.DllImport("kernel32.dll")] - private static extern bool IsDebuggerPresent(); - - [System.Runtime.InteropServices.DllImport("kernel32.dll")] - private static extern bool CheckRemoteDebuggerPresent(IntPtr hProcess, ref bool isDebuggerPresent); - - internal static void Validate() - { - if (IsDebuggerPresent()) - Environment.FailFast("Tamper detected."); - - bool debuggerPresent = false; - CheckRemoteDebuggerPresent(IntPtr.Zero, ref debuggerPresent); - if (debuggerPresent) - Environment.FailFast("Tamper detected."); - - // Timing check: if a method takes >10x expected time, a debugger is stepping - var sw = System.Diagnostics.Stopwatch.StartNew(); - SomeFrequentlyCalledMethod(); - sw.Stop(); - if (sw.ElapsedMilliseconds > 100) - Environment.FailFast("Tamper detected."); - } -} -``` - -**Trade-offs:** -- `IsDebuggerPresent()` is trivially patched (NOP the call). Useful against casual inspection, not determined attackers. -- Timing checks can false-positive on slow machines. Use conservative thresholds. -- `Environment.FailFast()` terminates the process immediately — no data corruption, no recovery. - -### 5.2 Anti-Tampering (est. 1 day) - -```csharp -// Method body CRC check — runs at startup -// Obfuscator can inject this automatically (ArmDot) -internal static void VerifyMethodBodies() -{ - var assembly = Assembly.GetExecutingAssembly(); - var location = assembly.Location; - var bytes = File.ReadAllBytes(location); - - // Check known CRC offsets (computed at build time) - foreach (var (offset, expectedCrc) in KnownChecksums) - { - var actualCrc = Crc32.Compute(bytes, offset, MethodBodyLength); - if (actualCrc != expectedCrc) - Environment.FailFast("Binary integrity check failed."); - } -} -``` - -### 5.3 Environment Validation (est. 0.5 day) - -```csharp -// Detect common analysis environments — NOT bulletproof, raises the bar -internal static bool IsAnalysisEnvironment() -{ - var suspiciousProcesses = new[] { "dnSpy", "ILSpy", "x64dbg", "x32dbg", "ollydbg", "procmon", "wireshark" }; - var running = System.Diagnostics.Process.GetProcesses() - .Select(p => p.ProcessName.ToLowerInvariant()); - if (running.Any(p => suspiciousProcesses.Contains(p))) - return true; - - var vmIndicators = new[] { "VMware", "VirtualBox", "Hyper-V", "Sandboxie" }; - var wmi = new System.Management.ManagementObjectSearcher("SELECT * FROM Win32_ComputerSystem"); - foreach (var obj in wmi.Get()) - { - var manufacturer = obj["Manufacturer"]?.ToString() ?? ""; - if (vmIndicators.Any(v => manufacturer.Contains(v, StringComparison.OrdinalIgnoreCase))) - return true; - } - - return false; -} -``` - -**Privacy note:** Environment checks that query WMI or enumerate processes require the app to have no network exfiltration of this data. Log locally only. Do NOT send "user is in a VM" telemetry — that's a privacy violation and potential legal issue. - -### 5.4 Preventing Memory Dumps (est. 0.5 day, limited effectiveness) - -```csharp -internal static void ProtectSensitiveMemory() -{ - // Encrypt license keys in memory when not in use - // Zero memory after use - // Use SecureString for transient secrets -} -``` - -**Reality check:** Against a determined attacker with admin rights, memory protection is bypassable. The value is in raising the bar, not creating an impenetrable wall. Focus effort on: -1. String encryption (hides API endpoints, keys, config). -2. Control flow obfuscation (makes logic hard to follow). -3. Polar server-side validation (license checks hit Polar's API, not our code). - -### 5.5 Secrets Management - -| Secret | Storage | Notes | -|--------|---------|-------| -| YouTube OAuth ClientSecret | Never in binary. Server-side only. | Client uses OAuth device flow; the secret lives on your auth server. | -| Code-signing certificate | CI/CD secrets only. | Never in source. | -| Polar access token (for uploads) | CI/CD secrets only. | Used in pipeline to upload new .exe to Polar. | -| User tokens (DPAPI) | `%APPDATA%\ytLlive\ytLlive.auth` | OS-level encryption. Never sent anywhere. | - -**Principle:** The client binary contains zero secrets that matter. Polar handles license validation server-side. The binary is a consumer of Polar's API, not a holder of credentials. The `OrganizationId` in `PolarLicenseService` is not a secret — it's required by Polar's public validation endpoint. - ---- - -## 6. Legal and Operational Measures - -### 6.1 EULA (est. 1 day, lawyer review recommended) - -Key clauses: - -``` -1. LICENSE GRANT: Limited, non-exclusive, non-transferable license to use the software. -2. RESTRICTIONS: No reverse engineering, decompilation, disassembly, or circumvention - of technical protection measures. No redistribution, sublicensing, or rental. -3. INTELLECTUAL PROPERTY: All rights reserved. The software is protected by copyright - law and international treaties. -4. TERMINATION: License terminates automatically upon breach. Upon termination, - you must destroy all copies. -5. DISCLAIMER: Software provided "AS IS" without warranty. -6. LIMITATION OF LIABILITY: Not liable for damages arising from use. -7. GOVERNING LAW: [Your jurisdiction]. -``` - -**Enforceability:** EULAs that prohibit reverse engineering have been upheld in the US (DMCA §1201) and EU (Copyright Directive Art. 6). They're less enforceable in some jurisdictions (e.g., Germany has compulsory decompilation rights under §69e UrhG for interoperability). A lawyer should review for your target markets. - -### 6.2 DMCA / Takedown - -1. **Register copyright** with the US Copyright Office ($65 per work). Required for statutory damages in infringement suits. -2. **Monitor** GitHub, torrent sites, and software sharing sites for pirated copies. Set up Google Alerts for "LlamaCasty download", "LlamaCasty crack". -3. **DMCA takedown** to GitHub/hosting provider when found. Template: - ``` - Subject: DMCA Takedown Notice — Copyright Infringement - I am the copyright owner of the software "LlamaCasty". - The following repository contains unauthorized copies: - [URL] - I have a good faith belief that the use is not authorized. - I swear under penalty of perjury that this information is accurate. - ``` -4. **DMCA to Google** to delist pirate download pages. - -### 6.3 Telemetry for Unauthorized Use (est. 0.5 day, optional) - -```csharp -// Optional: anonymized heartbeat that detects unauthorized distribution -// Only send: installation GUID (random, stored locally), version, license tier -// DO NOT send: file paths, user names, screen content, keystrokes -internal static async Task SendHeartbeatAsync() -{ - var id = GetOrCreateInstallationId(); // Random GUID, stored in Settings - var version = Assembly.GetExecutingAssembly().GetName().Version; - // POST to your API — response can include "deactivate" flag -} -``` - -**Privacy:** Heartbeat must be disclosed in the EULA and privacy policy. Must not collect PII. Must be opt-out-able in EU (GDPR). Consider whether this is worth the complexity for a solo dev — Polar already tracks validation counts per key, which gives you usage data without custom telemetry. - ---- - -## 7. Fallback and Recovery - -### 7.1 If Protection Is Bypassed - -**Tier 1 — Casual piracy (someone shares the .exe):** -- Monitor download sites, issue DMCA takedowns. -- Polar can disable the file benefit — old download URLs stop working. -- Push a new version to Polar; old versions become obsolete. -- No kill-switch needed — version obsolescence is sufficient. - -**Tier 2 — Cracked version distributed (license check bypassed):** -- Revoke the compromised license key via Polar dashboard (or API: `POST /v1/license-keys/{id}/revoke`). -- Rotate the key — previous key stops validating immediately. -- Push emergency update with new validation logic. -- The cracked version is now outdated; Polar's API rejects old keys. - -**Tier 3 — Full decompilation and source reconstruction:** -- This means obfuscation failed. Audit your obfuscation settings. -- Switch from Eazfuscator to ArmDot (virtualization is harder to reverse). -- Add server-side validation for critical features (license, updates). -- Accept that a determined attacker with enough time will always win — focus on making it not worth their time. - -### 7.2 Emergency Update Push - -**Via Polar (no custom infrastructure):** -1. Upload new `.exe` to Polar dashboard as updated file benefit. -2. Disable old file benefit — existing customers lose access to old download URL. -3. Customers with valid license keys see the new version in their Polar purchase page. -4. Optional: Use Polar webhook to notify a lightweight endpoint → push in-app notification. - -**Via GitHub Releases (fallback):** -1. Create a new release on GitHub with the updated `.exe`. -2. App checks GitHub Releases API for latest version (if Polar's file version metadata isn't exposed). -3. Download link points to Polar's customer portal (not direct GitHub download). - -### 7.3 Kill-Switch Ethics - -A kill-switch is legally and ethically complex: -- **Legal:** You can deactivate your own software per your EULA. Users agreed to it. -- **Ethical:** Don't deactivate without warning. Give 30 days notice. Provide data export. -- **Practical:** A kill-switch that bricks the app without notice will generate bad press and potential legal action. -- **Recommendation:** Use Polar's key revocation + version obsolescence instead of kill-switches. Let the version go stale rather than actively disabling it. - ---- - -## Summary: Feasibility Assessment - -| Step | Time | Difficulty | Solo Dev? | Polar Handles? | -|------|------|------------|-----------|----------------| -| Assembly split | 2–3 days | Medium | Yes | No | -| Obfuscation-friendly patterns | 1–2 days | Low | Yes | No | -| Attribute placement | 1 day | Low | Yes | No | -| Build pipeline | 1 day | Medium | Yes | No | -| Obfuscator integration | 2–3 days | Medium | Yes | No | -| XAML/BAML testing | 1 day | Medium | Yes | No | -| CI/CD pipeline | 1 day | Medium | Yes | No | -| Code signing setup | 0.5 day | Low | Yes | No | -| License key setup (Polar) | 0.5 day | Low | Yes | **Yes** | -| File hosting (Polar) | 0.5 day | Low | Yes | **Yes** | -| Checkout (Polar) | 0 | None | Yes | **Yes** | -| Customer portal (Polar) | 0 | None | Yes | **Yes** | -| Activation limits (Polar) | 0.5 day | Low | Yes | **Yes** | -| Key rotation/revocation (Polar) | 0 | None | Yes | **Yes** | -| Update mechanism | 1 day | Medium | Yes | Partial | -| Anti-debug/anti-tamper | 1–2 days | Medium | Yes | No | -| Environment validation | 0.5 day | Low | Yes | No | -| EULA (draft + review) | 1–2 days | Low | Lawyer recommended | No | -| DMCA preparation | 0.5 day | Low | Yes | No | -| **Total** | **~12–18 days** | | **Yes** | **~5 days saved** | - -**Bottom line:** Polar eliminates ~5 days of custom infrastructure (licensing backend, checkout, customer portal, device management, tax compliance, file hosting). What remains is purely our protection layer (obfuscation, code signing, anti-tamper) and legal measures. - -The most cost-effective approach for a solo dev: - -1. **Do (core):** Assembly split + Eazfuscator (Services.dll only) + code signing + Polar license keys + Polar file hosting. Stops 95% of casual piracy. (~7 days) -2. **Do (hardening):** Anti-debug checks + Polar activation limits (device caps). Adds another layer. (~3 days) -3. **Skip (for now):** Environment VM detection, memory protection, complex kill-switches. The cost/benefit doesn't work at indie scale. These are for enterprise or high-value targets. - -The determined reverse engineer with IDA Pro and unlimited time will always win. The goal is to make the cost of cracking exceed the value of the software — for a solo dev product, that bar is low. Focus on making a great product that people want to pay for. +- **⛔ No EV certificate.** Microsoft removed the EV SmartScreen instant-bypass in March 2024, so + EV ($400+/yr) buys exactly what OV (~$150–300/yr) buys — and a bought cert is a 460-day recurring + line item whose key must live on an HSM or hardware token. +- **No certificate at all** is the interim state: ship unsigned and accept the "unknown publisher" + line. winget spares the browser prompt, not the publisher line, so signing is what makes the + channel feel legitimate — the reason it is in the year-one budget. +- **Signing never clears the warning on day one** — only download volume does. 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 `, and confirm the hash in the manifest + matches the uploaded zip. + +## 4. 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. + +## 5. 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.** + +## 6. Effort, once + +| Step | Effort | +|---|---| +| Publish profile + portable mode | 0.5 day | +| GitHub Release + hash + version notice | 0.5 day | +| First winget manifest (`wingetcreate new`) | 0.5 day, then ~15 min per release | +| Azure Trusted Signing profile + CI step | 0.5 day | +| Clean-machine + `winget` smoke test | 0.5 day | diff --git a/HANDOFF.md b/HANDOFF.md index 2a1f341..3a442dc 100644 --- a/HANDOFF.md +++ b/HANDOFF.md @@ -1,40 +1,36 @@ -# HANDOFF — 2026-10-30 (scoreboard built to the polling policy; two wiring items left) +# HANDOFF — 2026-10-31 (commercial plan settled in docs; next code unit is the entitlement seam) ## State -- Branch `main`. The audio dock is shipped and pushed. **The scoreboard is built** in ten units, - local commits only: tallies (`Models/ScoreboardState`, session-scoped), lifecycle - (`ScoreboardRecorder`: start clears, End freezes), layout (`Models/ScoreboardLayout`: coupled - edges resolved at render time, shrink-then-wrap, never scroll), renderer - (`Services/Compositor/ScoreboardRenderer`: plate + text colour + opacity + 14–24px, cached), - tile selection (`Models/ScoreboardTiles`, opt-in, money per currency), the cached presenter, the - layer itself (`SourceType.Scoreboard`, strip geometry, resolver case), the properties panel - (eight fact switches + colours + plate + size, theme widgets only), the Analytics scope - (`yt-analytics.readonly`, incremental, `HasAnalyticsScope`), and the polling policy - (`Models/ScoreboardCadence`). -- Fixed on the way: the chat feed now reaches the tallies (the recorder had no caller). +- Branch `main`. Pushed through `c72b61b`; **`90d5140` + this commit are local only** (push at a + checkpoint, not per commit). +- **Commercial plan is settled and written down**: freemium (free = whole app at a 921,600 px / + 30 fps profile, stream **and** recording), Pro = $10/mo or $400 lifetime through **Polar** (key + bound to the buyer's YouTube channel ID via a claim page, 2 activations, 7–14 day lease), one + portable zip + **winget** as the primary channel, **Steam and the Store parked**, Azure Trusted + Signing, Founding 200 promo. New single source of truth: [`docs/ai/commercial.md`](docs/ai/commercial.md). +- Docs consolidated this unit: `product.md` (money section → pointer), `decisions.md` (banners + + history), `index.md`, `ai.md`, `README.md`, `Distribution.md` (rewritten to release mechanics, + 736 → 120 lines), `TASKS.md` (five new rows), `capture.md` (GPL line falsified by the proprietary + EULA), plus gitignored `MARCOM.md` / `MONETIZATION.md`. Site repo: `AGENTS.md`, `README.md`, + `build-this.md`. +- Scoreboard: built, unchanged from the last handoff (HTTP providers + goal inputs still missing). ## Next action -1. **HTTP providers** behind the cadence policy: watch hours (`reports.query`, - `metrics=estimatedMinutesWatched`) and subscriber count (`channels.list?mine=true&part=statistics`), - both behind an interface with a fake for tests, plus the timer that calls - `ScoreboardCadence.Due(...)` and writes the values into `ScoreboardTiles`. -2. **Goal-target inputs** in the properties panel (the tiles read `SubscriberGoal`/`WatchHourGoal` - but nothing sets them yet). -3. **On-screen check** of the strip: one line at 24px, plate/opacity, and the edge swap against the - social bar. +1. **Entitlement seam** — `IEntitlement` over Polar activate/validate/deactivate, channel-ID match, + 2 activations, lease + offline grace, `--activate LCYT-…` CLI, fail closed. One Good Dog test. +2. Then the **OutputProfile gate** (one profile consumed by both the stream encoder and the + recorder; greyed tier entries; never downgrade mid-stream), the licence UI, and system messages. +3. Scoreboard HTTP providers + goal inputs whenever the creator queues them. ## Landmines -- **Not every hex is a theme colour.** `BorderColorPalette` and the chat/socials config defaults - are USER DATA — they keep their literals. On-stream graphics now come from `OutputPalette`, - never from a literal or from app resources (the pump thread must not touch them). -- A red settings control is a bug now — red means live/record. +- **Nothing may gate a feature.** The only paid delta is the output profile. `ShowBrandFlash` stays + `false` and entitlement must never enable it. +- **The site publishes the model, never the numbers** — and its CTA ("Coming soon to the Microsoft + Store") stays until the creator says the download exists. +- **No self-updater, by policy**: `winget upgrade` + a version banner. `StorePackagingTests` asserts it. - Pre-existing: the **full test suite hangs** (WASAPI teardown). Per-class runs are the way. - -## Pending - -- **Launch and eyeball**: fonts render (pack-URI resolution cannot be proven in the test host), - the blue accent, blue-staged / red-live tiles, and mono telemetry. -- Vertical-tier camera check; webcam-cadence take in the failing geometry. +- Not every hex is a theme colour — `BorderColorPalette`/chat defaults are USER DATA; on-stream + graphics come from `OutputPalette`. A red settings control is still a bug. diff --git a/README.md b/README.md index 0791278..a5e619f 100644 --- a/README.md +++ b/README.md @@ -36,13 +36,18 @@ setup — they need one that's ready for *theirs*. So I built LlamaCasty around The general-purpose tools are the right choice when you need everything. LlamaCasty is for when you just want to get your show on air. -## Free, and staying that way +## Free, and the paid edition -LlamaCasty is free forever: no watermark, no branding on your stream, no subscription, no -LlamaCasty account. It is commercial software (see [License](#license)) — try it, stream with it, -experiment with it. If it earns a spot in your setup and you'd like to say thanks, there's a -coffee jar and a merch store linked from https://llamacasty.com. Completely optional, never -expected, and the app will never ask. +The free version is the whole app, not a trial: every feature, every scene, every overlay, no +watermark, no LlamaCasty account, and nothing in your stream advertising us. It stays free. + +A paid edition exists for creators who need more output quality — free streams and records at up to +720p30, and Pro unlocks the higher tiers. **No feature is ever gated behind it.** The app shows the +current price at the point of sale; the website deliberately does not. + +If it earns a spot in your setup and you'd like to say thanks, there's a coffee jar and a merch +store linked from https://llamacasty.com. Completely optional, never expected, and the app will +never ask. ## How it's built @@ -96,7 +101,7 @@ What v1 ships: the five-scene control surface, webcam + screen capture, media + sources, chat on stream, the social bar, mic/desktop audio with auto-ducking and TRAX background music, local recording, quality tiers including vertical 9:16, one-click go-live + scheduling, stream resilience, monetization awareness (YPP tracker), animated alerts, and webcam background -removal and blur — all free and ungated. +removal and blur — every feature in the free tier, with a paid edition that buys output quality. ## Structure @@ -118,7 +123,9 @@ queue; `HANDOFF.md` is the current state. Each code directory carries an `index. ## License LlamaCasty is **proprietary commercial software** — see [`LICENSE`](LICENSE) for the full End User -License Agreement. The free tier streams at up to **720p30**; a paid licence unlocks 1080p. +License Agreement. The free tier streams **and records** at an output profile of up to **921,600 +pixels at 30 fps** (720p30 landscape, 720×1280 vertical); a paid licence unlocks every shipped tier, +including 1080p60. Prices, entitlement and channels: `docs/ai/commercial.md`. Earlier development builds were published under GPL-3.0 and remain covered by that licence for the versions that shipped under it; nothing here revokes that. diff --git a/TASKS.md b/TASKS.md index 463eac7..364dbd8 100644 --- a/TASKS.md +++ b/TASKS.md @@ -32,13 +32,18 @@ renderer mirrors `Services/ChatBoxRenderer.cs`. One Good Dog test. | 49 | ☐ | Chat profanity filter: local, opt-in, **non-persistent**, user-supplied word list | [`task-49`](TASKS/task-49-chat-profanity-filter.md) | | 50 | ☐ | One-button publish of the local recording (resumable upload) | [`task-50`](TASKS/task-50-publish-recording.md) | | 53 | ☐ | Output-format contract: pin every tier with a geometry test, prove 9:16 end-to-end, recorded file matches output | [`task-53`](TASKS/task-53-output-formats.md) | -| 54 | 🔶 | Licensing posture changed 2026-10-30: **freemium** (720p30 free, $10/mo or $400 lifetime) — EULA + privacy shipped; site copy still says free forever, entitlement/keys not built | [`task-54`](TASKS/task-54-marcom-free-merch.md) | +| 54 | 🔶 | Freemium: EULA + privacy shipped; **no licence path built** — needs the entitlement seam, the output-profile cap and the licence UI below; site copy still says free forever | [`task-54`](TASKS/task-54-marcom-free-merch.md) | +| — | ☐ | **Entitlement seam** — `IEntitlement` over Polar activate/validate/deactivate, channel-ID match, 2 activations, 7–14 day lease, `--activate` CLI | [`commercial`](docs/ai/commercial.md) | +| — | ☐ | **OutputProfile gate** — ≤921,600 px / ≤30 fps for stream **and** recording, greyed tier entries, live re-resolve, never mid-stream | [`commercial`](docs/ai/commercial.md) | +| — | ☐ | **Licence UI** — claim page, four states, 7/1/0-day notices, portal link, licence ID in About | [`commercial`](docs/ai/commercial.md) | +| — | ☐ | **System messages** — `SystemMessage` + `MessageCenter`, strip above the dock, dedupe by key, short history, never composited | [`ui-surface`](docs/ai/ui-surface.md) | +| — | ☐ | **Packaging** — portable zip, GitHub Release, winget manifest, Azure Trusted Signing | [`Distribution.md`](Distribution.md) | | 9·5 | ☐ | Webcam identity key reconciliation | [`task-09`](TASKS/task-09-live-stream-management.md) | | 14 | 🔶 | Creator feedback batch — branch 3+ | [`task-14`](TASKS/task-14-creator-feedback-batch.md) | | — | ☐ | Small: `StreamConfig` defaults (`TargetBitrate=6000`, `1920x1080`) are stale — the dropdown drives `StreamHealth` | this line | | 36 | ☐ | **Gold pass** — the v1 gate, always the last unit (see 1.0 gates) | [`task-36`](TASKS/task-36-gold-pass.md) | -| 48 | ⏸ | Distribution & packaging — deferred to near-v1 (creator ruling 2026-10-26); route contradiction settled in one sweep then | [`task-48`](TASKS/task-48-distribution-msix.md) | -| 10 | ❌ | Retired with free-forever; a Polar teardown runs only if a paid tier ever returns | [`task-10`](TASKS/task-10-monetization.md) | +| 48 | ⏸ | Distribution & packaging — route settled 2026-10-31 (winget/portable first; Steam + Store parked). Build steps: [`Distribution.md`](Distribution.md); starts after the entitlement seam | [`task-48`](TASKS/task-48-distribution-msix.md) | +| 10 | ❌ | Retired 2026-10-08; superseded by the TASK 54 rows above — the Polar design it described is rebuilt as a lease (`commercial.md`), not revived as written | [`task-10`](TASKS/task-10-monetization.md) | ## Pending the creator's hands (not programming) diff --git a/TASKS/task-10-monetization.md b/TASKS/task-10-monetization.md index c908e12..c37159e 100644 --- a/TASKS/task-10-monetization.md +++ b/TASKS/task-10-monetization.md @@ -1,5 +1,11 @@ # TASK 10 — Monetization: watermark-only paid delta (branding flash) +> ⛔ **RETIRED — 2026-10-30.** Both halves of this file are dead: the watermark/branding-flash +> delta (retired 2026-10-30) and Store IAP as the billing provider (Store is parked, third channel, +> 2026-10-31). The paid delta is **output quality** now, sold through **Polar**. Live policy: +> [`docs/ai/commercial.md`](../docs/ai/commercial.md); cause of death: +> [`docs/ai/decisions.md`](../docs/ai/decisions.md). Nothing here is a build instruction. + > Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. **Goal:** a **paid tier that removes the branding flash and nothing else.** Every feature is free diff --git a/TASKS/task-48-distribution-msix.md b/TASKS/task-48-distribution-msix.md index 16fd46b..615fa64 100644 --- a/TASKS/task-48-distribution-msix.md +++ b/TASKS/task-48-distribution-msix.md @@ -1,5 +1,12 @@ # TASK 48 — Distribution & packaging: route decision, MSIX, signing +> ⛔ **THE ROUTE IN THIS FILE IS SUPERSEDED — 2026-10-31.** Distribution is **direct download from +> `llamacasty.com` (GitHub Releases) + winget primary**; **Steam and the Microsoft Store are parked**, +> third and fourth. Store IAP is not the billing path — **Polar** is, and the paid delta is output +> quality. Live policy: [`docs/ai/commercial.md`](../docs/ai/commercial.md); mechanics: +> [`Distribution.md`](../Distribution.md); cause of death: [`docs/ai/decisions.md`](../docs/ai/decisions.md). +> The MSIX build steps below are still accurate and remain the parked Store channel's artifact. + > 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 diff --git a/TASKS/task-51-webcam-bg-effects.md b/TASKS/task-51-webcam-bg-effects.md index 44d9b56..16c3328 100644 --- a/TASKS/task-51-webcam-bg-effects.md +++ b/TASKS/task-51-webcam-bg-effects.md @@ -39,8 +39,10 @@ delivery shape changed: same posture as the ffmpeg bundling in TASK 48 item 1.) 2. **No hardware requirement.** CPU path must work; DirectML is an optional accelerator. 3. **Per-element (Meld-style), not global.** Each webcam element carries its own effect. -4. **No feature gating.** Everything is free (free-forever ruling 2026-10-08, TASK 54 — the old - "only the branding flash is paid" line here was superseded). +4. **No feature gating.** Background effects are in the free tier (2026-10-31 freemium keeps every + feature free; the paid delta is output quality only — `docs/ai/commercial.md`). The old "only the + branding flash is paid" line here was superseded in 2026-10-08, and the flash delta itself died + on 2026-10-30. 5. **Scene change / webcam add/remove must not crash or stall the pump** — a failing model run skips to passthrough, never drops the layer. diff --git a/TASKS/task-54-marcom-free-merch.md b/TASKS/task-54-marcom-free-merch.md index 18beafa..c93ac8b 100644 --- a/TASKS/task-54-marcom-free-merch.md +++ b/TASKS/task-54-marcom-free-merch.md @@ -1,5 +1,13 @@ # TASK 54 — Marcom round: free-forever positioning + merch (queued 2026-10-08) +> ⛔ **THE FREE-FOREVER RULING IN THIS FILE IS SUPERSEDED — 2026-10-30.** The app is **freemium**: +> every feature is still free, and a paid edition exists for higher output quality (free is capped +> at a 921,600 px / 30 fps profile, stream **and** recording). Live policy: +> [`docs/ai/commercial.md`](../docs/ai/commercial.md). Cause of death: +> [`docs/ai/decisions.md`](../docs/ai/decisions.md) → Monetization. Read the rest of this file as +> the 2026-10-08 record — the merch/gratuity half still stands, the "free forever, no paid tier" +> half does not. + > Catalog: [`TASKS.md`](../TASKS.md) — status and requirements live here. **Why:** the monetization decision changed. LlamaCasty launches **watermark-free and free as in beer, diff --git a/ai.md b/ai.md index f51f9df..db1be6f 100644 --- a/ai.md +++ b/ai.md @@ -8,8 +8,9 @@ A Windows-only WPF (.NET 8) app that live-streams to YouTube. Five fixed scenes (Starting / Live / BRB / Chat / Ending), a pure C# software compositor over an always-1920×1080 -master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Free forever, GPL-3.0, -no account creation — it links an existing YouTube channel. One-click go-live is the product; +master, a bundled-ffmpeg encoder pushing RTMP, and a WASAPI audio chain. Freemium — free is the +whole app at a 720p30 output profile, Pro buys higher output quality — under a proprietary EULA, +with no account creation: it links an existing YouTube channel. One-click go-live is the product; OBS is the escape hatch for anything bigger. ## Build, run, test @@ -50,7 +51,8 @@ C# / WPF (.NET 8) following MVVM: | Social bar, alerts, chat overlays | [`docs/ai/overlays.md`](docs/ai/overlays.md) | anything drawn on top of the output | | Go-live, broadcasts, health, chat API | [`docs/ai/youtube-api.md`](docs/ai/youtube-api.md) | go-live, broadcast lifecycle, health polling, streaming close-out | | Control surface, theming, transitions | [`docs/ai/ui-surface.md`](docs/ai/ui-surface.md) | anything the creator looks at | -| Brand, licensing, monetization, distribution | [`docs/ai/product.md`](docs/ai/product.md) | branding, pricing, licensing, packaging | +| Brand, packaging, legal posture | [`docs/ai/product.md`](docs/ai/product.md) | branding, naming, packaging behaviour, legal obligations | +| Prices, entitlement, channels, signing, promo | [`docs/ai/commercial.md`](docs/ai/commercial.md) | anything about money: pricing, licence keys, output cap, winget/Steam/Store, costs | | Hosts, build commands, test map | [`docs/ai/environment.md`](docs/ai/environment.md) | building, running, adding a test | | Routes for every subsystem | [`docs/ai/index.md`](docs/ai/index.md) | you want the full table with a "read when" per file | diff --git a/docs/ai/capture.md b/docs/ai/capture.md index 5756321..92c9540 100644 --- a/docs/ai/capture.md +++ b/docs/ai/capture.md @@ -312,8 +312,9 @@ This replaces the old five-seeder cluster (`Seed{Starting,Brb,Ending,Chat}Backgr `verify_against_mediapipe.py` feeds the same frame to MediaPipe's `ImageSegmenter` (original .tflite) and onnxruntime (our .onnx): max |diff| 5e-5, 100% binary agreement on two images. ⛔ Do NOT swap in PINTO model-zoo #109 (it's ML Kit's model, extracted — not Apache provenance). - obs-backgroundremoval is GPL-3.0 — license-compatible since LlamaCasty went GPL-3.0 (2026-10-09), - but none of its code is used; if any is ever borrowed, it needs its copyright notice kept. + obs-backgroundremoval is GPL-3.0 and therefore **licence-incompatible since the app went + proprietary (2026-10-30)** — none of its code is used, and none may be borrowed into this + codebase without a licence ruling first. Google's model is the provenance that matters. - **Contract:** input `[1,144,256,3]` RGB /255 NHWC → output `[1,144,256,1]` person probability. The whole frame is stretched (not cropped) to 256×144, so the mask maps back by normalized coordinates. - **Shipping:** `Assets/Models/selfie_segmenter_landscape.onnx` is an **EmbeddedResource** (in git, diff --git a/docs/ai/commercial.md b/docs/ai/commercial.md new file mode 100644 index 0000000..e6ba8e3 --- /dev/null +++ b/docs/ai/commercial.md @@ -0,0 +1,152 @@ +# Commercial: pricing, entitlement, channels + +The single source of truth for what LlamaCasty charges, what a licence entitles, and how it +ships. Read before any pricing, licensing, distribution, storefront or promo work. Cause of death +for every retired path: [`decisions.md`](decisions.md). + +## Model + +- **Free tier — every feature, every scene, every overlay**, free forever, no account, no + watermark, nothing nagged inside the output. It is not a trial. +- **Pro — $10/month or $400 lifetime.** No annual tier. +- **The only delta is output quality.** Features are never gated; a payment buys pixels. +- **No watermark, ever**, in either edition. The branding flash is retired as a paid delta and + stays off (`ShowBrandFlash`, default `false`). +- **Clean numbers, no `.99`**, no discount codes, no countdowns, no fake scarcity. +- **The site publishes the model, never the numbers** — "free forever, every feature unlocked, a + paid edition with higher output quality at launch". Price, tier name, cap and frame rate are + published by the **app**, at the point of sale, not by the page. + +## Output profile — the one entitlement rule + +The cap is a pixel budget plus a frame rate, not the word "720p": + +| | Free | Pro | +|---|---|---| +| Pixels | ≤ **921,600** | any shipped tier | +| Frame rate | ≤ **30 fps** | any shipped tier | +| Landscape | 1280×720 @30 | 1280×720 @60 · 1920×1080 @30 · @60 | +| Vertical 9:16 | 720×1280 @30 | 1080×1920 @60 | + +- **Records too.** The cap governs the file the creator keeps as well as the stream; otherwise + free 1080p recording is a re-upload hole. One `OutputProfile`, resolved once per session and + consumed by **both** the stream encoder and the recorder — never two checks. +- **Fail closed.** Unresolved, unreadable or unvalidated entitlement = free tier. Never Pro. +- **Never downgrade a running stream.** The gate applies to the next start; pulling quality out + of a live broadcast is a worse bug than one free 1080p session. +- **The greyed menu entry is the sales surface.** It stays clickable, says what it costs, opens + checkout, and is labelled ("720p30 · free tier") — never silently missing, because silence + reads as broken. +- **Activation is live**, not restart-gated: activating enables the high tiers and a lapse + (cancel, revoke, sign-out, channel switch) greys them again on the next refresh. +- **Bitrate follows the profile** — a capped user is not offered a 1080p bitrate. + +## Entitlement + +**Polar is the merchant of record and the only source of truth.** It takes the money, remits +VAT/GST, handles refunds and chargebacks, and holds the keys. Nothing of ours sits in the middle. + +- **Key** — `LCYT-` prefix; org `c05fb364-…` and the endpoint are in `CREDENTIALS.md`. No + product is created yet. +- **Bound to the creator's YouTube channel ID** (`UC…` from `channels.list?mine=true`) — the + **active** brand account's channel, never the `@handle`. Written once through a **claim page** + after purchase (OAuth), not prefilled at checkout: the buyer's browser may be signed into a + different Google account, and that is manual reconciliation forever. +- **Two activations**, any machine, one channel. A second channel is a second key. Two keys for + one channel is never sold — it doubles the support surface and invites "give one to a friend". +- **Runtime rule:** Pro requires `licence.channelId == connected.channelId`. Equal → Pro. + Anything else → free tier, working normally, with *"This licence belongs to another channel"* + and a link to the Polar customer portal. Never an error wall, never a blocked stream. +- **Lease, not a permanent cache:** validate on launch and daily; **7–14 days offline grace**; + no network past grace = free tier until Polar answers. Never lock a paying user out on a DNS + failure. +- **Four states, and nothing else:** valid → Pro · expired → free with a banner · activation + limit reached → "manage devices" portal link · mismatch or offline → free, explained. +- **CLI activation** — `--activate LCYT-…` so silent and fleet installs never meet a dialog. + Interactive entry in Settings is the secondary path. +- **Notice cadence** — 7 days out, 1 day out, and on the day, inline in the go-live panel with a + date and a portal link. Recording gets the banner only. Never modal, never blocking a Start, + never orange (orange is the dev-only label colour). + +## Channels + +**One artifact, three ways to get it.** A self-contained single-file `LlamaCasty.exe` plus the +bundled `ffmpeg\` folder, zipped. No .NET install, no admin, no installer framework. Trimming +stays off (WPF reflection), so expect ~110–150 MB. + +1. **Direct download** — `llamacasty.com` links the zip; the binaries live on **GitHub Releases**, + not the droplet (free CDN, durable URLs, release notes). +2. **winget — the primary channel.** Manifest in `microsoft/winget-pkgs`, `InstallerType: + portable`, pinned SHA-256. That hash pin is the anti-tamper wall: the official channel delivers + exactly the published artifact or fails. One `wingetcreate update` PR per release. Optional: + Scoop and Chocolatey manifests over the same zip. +3. **Steam — parked at $100.** Lifetime Pro as DLC only (Steam has no subscriptions), page public + two weeks before release, 30% cut, and price parity with the site. +4. **Microsoft Store — parked.** $19 Individual account; the only storefront that bills a + subscription natively and installs without a warning. Bans self-updaters, so a Store build + ships without one. + +**Not channels, ever:** Patreon or any subscription platform of our own, paywalled early access, +a paid beta, a Discord. Support is a public GitHub issue tracker. Content is the funnel; the +channel is only the checkout. + +**Portable mode:** a `portable.txt` marker beside the exe keeps the layout DB and settings in +`.\data\` instead of `%APPDATA%\ytLlive` — the USB-stick case, hung off `LayoutPathOverride`. + +## Signing + +- **Azure Trusted Signing, ~$9.99/month**, is the intended certificate: Microsoft-issued, accrues + SmartScreen reputation, enrols individuals after identity verification. +- **No EV certificate** — Microsoft removed the SmartScreen instant-bypass in March 2024. +- Until it is in place, builds ship unsigned and the "unknown publisher" line is accepted. winget + spares the browser prompt, not the publisher warning. +- Every release: same signing pass, SHA-256 in the manifest, licence ID visible in About. + +## Founding 200 promo + +Cash cost **$0**; it is priced in support time and forgone list revenue. + +- **90 days free** — a 100%-off Polar coupon, `duration = repeating, months = 3`. +- **Founder price lock** — their $10/month stays $10 permanently. +- **Credits + in-app Founder badge** (opt-in channel name on the site). +- **30-day passes** — unique keys carrying the issuer's channel, issued **one per 30 active days, + max 3 held**, activation limit 1–2, expiring 30 days **after activation**. Earned, never handed + out: a pass nobody uses teaches nothing. +- Optional **$250 lifetime** window in the first 90 days — converting a founder beats three + months of free. +- Reminders at day 75 and 85; the renewal cliff is where founders churn. +- Terms, one paragraph: one per person, passes expire 30 days after activation, no cash value, + revoked for resale or abuse, price lock void if the subscription lapses. + +## Costs + +| Item | Cost | +|---|---| +| Azure Trusted Signing | $9.99/month (~$120/yr) | +| Domain | ~$15/yr | +| Droplet | $0 — serves the existing site | +| GitHub Releases, winget, Scoop, Chocolatey | $0 | +| Polar | 5% + 50¢ per sale → $10 nets $9.00, $400 nets $379.50 | +| Steam Direct (optional) | $100 once, recoupable after $1,000 AGR | +| Microsoft Store (optional) | $19 once | + +Fixed year one ≈ **$135**, and the same every year after. **Break-even is two subscribers** +(15 subscriber-months ≈ $135); one $400 lifetime sale covers nearly three years. + +## Anti-piracy posture + +**Any check that runs on the user's machine can be removed.** The goal is a crack that is +pointless and a leak that is traceable — not an unbreakable binary. + +- **The free tier is the real defence.** If the only delta is pixels above 921,600, a cracked + build buys almost nothing and forfeits updates, support and the publisher's signature. +- **Hash-pinned delivery + signed builds** — a patched build cannot arrive through winget, and a + forged one is not signed as us. +- **Short leases** — a revoked or refunded key dies inside the grace window. +- **Licence ID in About** — a leaked build traces back to a purchase. + +⛔ **Rejected, do not re-propose:** a native helper DLL for "IP protection", obfuscation, hardware +locking, remote code that downloads and executes, and gating asset downloads as an auth check. +None of them make a licence enforceable; the first three add failure modes without buying +anything, the fourth needs the server this plan exists to avoid, and the fifth is Store policy +10.2.2. diff --git a/docs/ai/decisions.md b/docs/ai/decisions.md index e13afdb..04058b6 100644 --- a/docs/ai/decisions.md +++ b/docs/ai/decisions.md @@ -1,15 +1,18 @@ # Decisions log — retired paths, kept so they are not re-litigated One dated line per decision that **no longer governs**, plus the reason it died. Read only when -you are about to re-open one of them. Live rules live in the subsystem files; this is the -graveyard with cause of death. +you are about to re-open one of them. **Live commercial rules (prices, entitlement, channels, +signing, promo) are in [`commercial.md`](commercial.md)**; the other live rules live in the +subsystem files. This is the graveyard with cause of death. ## Monetization ### 2026-10-30 — freemium replaces free-forever (creator ruling) -Free tier caps output at **720p30**; **$10/month** or **$400 lifetime** unlocks 1080p. Reverses the -2026-10-08 free-forever ruling below, which was a *current decision*, not an absolute. +Free tier caps output at **720p30**; **$10/month** or **$400 lifetime** unlocks the higher tiers. +Reverses the 2026-10-08 free-forever ruling below, which was a *current decision*, not an absolute. +The live detail — pixel budget, stream + recording, channel binding, lease, notice cadence — is in +[`commercial.md`](commercial.md); do not re-derive it here. **The website publishes the model, never the numbers** (free forever, every feature unlocked, a paid edition at launch) — the terms may still change. The real plan is recorded internally in the site's @@ -33,23 +36,41 @@ edition at launch) — the terms may still change. The real plan is recorded int - **Rejected**: always-on watermark (obscurable — replaced by the flash), hard stream-time cutoffs (a stream dying mid-broadcast reads as broken), soft-limit nagging, freemium feature tiers, donation-only, and any resolution/quality ceiling. Auto step-down (TASK 33) is a *protect the - stream* feature, never a monetization penalty. -- **Polar** — $50 already paid, 5% + $0.50 Starter, merchant of record. Dormant, not - deleted-in-principle: no product, org ID, key prefix or discount code exists - (`d105dfa1-…`, `c05fb364-…`, `LCYT-`, `LLAMAFOUNDER`/`LLAMA50`, the portal-validate endpoint and - the 14-day offline-grace machine are all gone). `Distribution.md` §4.2/§4.5 keeps the design. + stream* feature, never a monetization penalty. ⚠️ **The quality-ceiling and feature-tier halves + were reversed on 2026-10-30** (freemium, output-quality delta); the watermark, cutoff and + nagging rejections still stand. +- **Polar** — $50 already paid, 5% + $0.50 Starter, merchant of record. **Live again as of + 2026-10-31** (`commercial.md`). The org ID `c05fb364-…`, the `LCYT-` key prefix and the + customer-portal validate endpoint are recorded in `CREDENTIALS.md`; no product, licence keys or + discount codes exist yet. The old `PolarLicenseService`/`Helpers/PolarLicense.cs` are still gone + — the 14-day-grace design has to be rebuilt (as a lease, not a permanent cache). - **Store IAP** was never implemented — `Windows.Services.Store` appears nowhere in the repo. - ⚠️ **Two obligations any future subscription would create** (ADA §6(h)): fulfil the whole period as marketed (breach → Microsoft may refund in full at its discretion), and **raising the price disables auto-renew** — the launch price is effectively locked for the product's life. -## Distribution — Store route RE-OPENED 2026-10-30 (Individual account) +## Distribution — Store PARKED as the third channel (2026-10-31) -**2026-10-30:** the Store is the route again, via a **Microsoft Individual developer account** -(not Company), so no legal entity is needed and the publisher line shows the verified individual -name. The creator will open the account; distribution work stays parked until then. The site copy -("Coming soon to the Microsoft Store") is therefore accurate again, and the 2026-10-22 -"Store is dead / direct + Steam" ruling below is superseded. +**2026-10-31:** **winget over a portable zip is the primary channel** — one self-contained +`LlamaCasty.exe` + bundled `ffmpeg\`, zipped, hosted on GitHub Releases, manifest in +`microsoft/winget-pkgs` with a pinned SHA-256. Steam is second and parked at $100 (lifetime Pro as +DLC only — Steam has no subscriptions). The **Microsoft Store is third and parked** until the +creator opens the **$19 Individual** developer account: it is the only storefront that bills a +subscription natively and installs without a SmartScreen warning, and it bans self-updaters. +Channels, costs, artifact and signing: [`commercial.md`](commercial.md). + +**Why the ordering inverted:** winget has no discovery, but neither does the Store for utilities, +and winget costs nothing, needs no account, no certification and no MSIX pipeline — while the +audience installs Windows tools from a command line. The site and the YouTube channel are the +funnel in both cases. + +## Superseded: Store was "the route again" — 2026-10-30 (ordering only) + +**2026-10-30:** a **Microsoft Individual developer account** (not Company) removed the legal-entity +blocker — the publisher line shows the verified individual name, and Partner Center does not +convert Individual → Company later. That finding stands; what it no longer implies is *primary*. +The site copy ("Coming soon to the Microsoft Store") stays true until the download page lands, then +the Store line becomes the secondary note. ## Superseded: Store route withdrawn 2026-10-22 @@ -58,20 +79,19 @@ name. The creator will open the account; distribution work stays parked until th New Mexico with a ~2-week SOS backlog, ~$143 Wyoming). Partner Center does **not** convert an Individual account to a Company account later, and an Individual account publishes under the verified legal name. The creator will not fund an entity for a nameplate. -- **2026-10-22 Velopack deleted** — it downloaded and executed, which Store policy 10.2.2 forbids. - The Store was going to update the app, so that premise died with the route: **the app now has no - update path**, and `StorePackagingTests` currently asserts that no updater exists — rewrite that - guard with the chosen updater, never leave it to veto one. -- **The MSIX package still builds** (159 MB `artifacts/LlamaCasty.msix`, submitted nowhere). The - route contradiction between `ai.md`'s older text and the TASKS files is settled in one sweep at - the distribution revisit, not patched piecemeal. +- **2026-10-22 Velopack deleted** — it downloaded and executed, which Store policy 10.2.2 forbids + (and which the 2026-10-31 plan keeps forbidden). The app has **no self-updater by policy**: + `winget upgrade` plus an in-app "newer version exists" banner is the update path, and + `StorePackagingTests` asserts that no self-updater returns. +- **The MSIX package still builds** (159 MB `artifacts/LlamaCasty.msix`, submitted nowhere) — it + is the parked Store channel's artifact. - **Signing**: Microsoft removed the EV instant-bypass for SmartScreen in March 2024, so EV buys - what OV buys; the 2026-10-22 ruling is **ship unsigned** and accept the "unrecognized publisher" - warning until download reputation accrues. Deferred, not rejected. + what OV buys. The 2026-10-22 ruling was *ship unsigned*; **2026-10-31 supersedes it with Azure + Trusted Signing (~$9.99/month)**, which is Microsoft-issued and accrues reputation. - **Steam when $100 exists**: Steam Direct $100/product (recoupable after $1,000 AGR), categories *Audio/Video Production* / *Player Tools* (OBS precedent), developer/publisher name self-chosen (no entity needed), 5+ 1920×1080 screenshots + capsule art, a build Valve runs, a 2-week live - page, 30% cut. + page, 30% cut, and no subscription support. ## Product surface — retired @@ -91,6 +111,11 @@ name. The creator will open the account; distribution work stays parked until th Do not build these; each was explicitly ruled out. +- **Anti-piracy theatre (2026-10-31)**: a native helper DLL for "IP protection", obfuscation, + hardware locking, remote code that downloads and executes, and gating asset downloads as an auth + check. None make a licence enforceable; see `commercial.md` → "Anti-piracy posture". +- **Commercial structures (2026-10-31)**: Patreon or any subscription platform of our own, paid + early access, a paid beta, a gated Discord, and a Pro trial (the free tier is the trial). - D3D11 swap compositor (superseded by the software compositor). - OBS-style source expansion — game capture, browser-source zoo, media playlist, VLC, colour-key, MIDI. The minimal source set is deliberate; creators who need more have graduated to OBS. diff --git a/docs/ai/index.md b/docs/ai/index.md index ef6268c..b15fe24 100644 --- a/docs/ai/index.md +++ b/docs/ai/index.md @@ -13,10 +13,11 @@ Read the file whose row matches the task. There is no required reading order. | [`capture.md`](capture.md) | The locked Background layer (screen/game/window capture, WinRT GraphicsCapture interop), the webcam seam + `CameraManager` sessions, MediaPipe background effects, and the vertical 9:16 tier's per-scene camera placement | Touching any frame source, the Background row, camera acquisition/conflicts, background effects, or vertical-tier camera geometry | | [`output.md`](output.md) | The bundled `ffmpeg.exe` and the encoder subprocess (args, probe order, seams), local recording (REC/ON-AIR, filenames, private-broadcast close-out), and the WASAPI capture → filter → mix → named-pipe audio chain (TRAX, sync offset, ducking, limiter) | Touching ffmpeg args/bundling, encoder or recording behaviour, REC/ON-AIR, the audio mix and its meters, or loudness/sync problems | | [`environment.md`](environment.md) | LAN hosts/IPs/SSH and the Gitea push recipe, the Windows `dotnet.exe` build + run commands, the test suite map, and the hermetic real-`MainWindow` test rule | Before building, running, adding a test, or reaching any box on the LAN | -| [`product.md`](product.md) | Brand/naming split, AI transparency, the GPL licensing prohibitions, the free-forever monetization posture, auth/account assumptions, and packaging/distribution | Branding, marketing, pricing, licensing, or packaging decisions | +| [`product.md`](product.md) | Brand/naming split, AI transparency, the licensing prohibitions, the freemium posture, auth/account assumptions, and packaging behaviour | Branding, naming, packaging, or a legal/third-party obligation | +| [`commercial.md`](commercial.md) | The live commercial policy: model and prices, the output-profile cap (stream **and** recording), Polar entitlement and channel binding, the channels (winget, Steam, Store), signing, the Founding 200 promo, costs, anti-piracy posture | Any question about money: pricing, licence keys, what free gets, how it ships, what a sale costs | | [`youtube-api.md`](youtube-api.md) | Go-live ordering + the broadcast-binding fix, the private-only lock, health parsing, end-stream pre-flight, Test mode, and chat-insert shape | Touching go-live, broadcast lifecycle, health polling, streaming close-out, or the chat service | | [`overlays.md`](overlays.md) | The social bar, the native alert box (six animations, alert clip, ticker) and the chat box conventions — everything composited over the frame | Adding or changing anything drawn on top of the output | -| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid tier and pricing, Polar, Store IAP, the Store route, the deleted updater, the rejected monetization ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled | +| [`decisions.md`](decisions.md) | Retired paths with cause of death: the paid-tier history and pricing, the free-forever era, Store IAP, the Store ordering, rejected monetization and anti-piracy ideas, and the permanent out-of-product list | You are about to re-open a decision that was already settled | | [`ui-surface.md`](ui-surface.md) | The control surface, scene strip, preview rail, hotkeys, broadcast drawer, toasts, transitions, brand logo, launch geometry, and the theme/properties-panel conventions | Changing anything the creator sees: layout, controls, theming, properties panel | ## Split-out yet to come diff --git a/docs/ai/product.md b/docs/ai/product.md index 53cee57..435e0a8 100644 --- a/docs/ai/product.md +++ b/docs/ai/product.md @@ -56,7 +56,9 @@ collaboration, not autonomous code generation. ## Licensing — proprietary since 2026-10-30 **LlamaCasty is closed-source commercial software.** `LICENSE` is the EULA; the paid tier exists -because the free tier caps output at **720p30** (creator ruling 2026-10-30, superseding free-forever). +because output quality is the paid delta — the free tier is capped at a 720p30 profile (creator +ruling 2026-10-30, superseding free-forever). Prices, entitlement and channels: +[`commercial.md`](commercial.md). - **Every GPL build already published stays GPL for that snapshot.** Attribution/§7 terms (`ADDITIONAL-TERMS.md`) are deleted and were GPL-only; the files are gone. @@ -88,18 +90,21 @@ Apply this to every UI decision: stream, not both; one reusable stream; one webcam; two mixer inputs). If a feature only sings on a high-end chassis, it doesn't ship — the OBS escape hatch is open by design. -## Monetization — free forever, no watermark +## Monetization — free tier + Pro output quality -**All features are free and ungated** (creator ruling 2026-10-08, TASK 54): no feature lock, no paid -tier, no watermark. Money comes from gratuity and merch, not gating. **No licence chatter reaches a -user** — no keys, no expiry, no licence UI. Support is email + GitHub issues; an in-app bug reporter -is out of product. `IsPremium` gates nothing in v1. +**Every feature is free and ungated** (creator ruling 2026-10-08, TASK 54, kept by the 2026-10-30 +freemium ruling): no feature lock, no watermark, and nothing licence-shaped in the output. The +paid delta is **output quality alone** — free is capped at a 720p30 profile, Pro unlocks the +higher tiers for **both the stream and the recording**. Prices, entitlement and channels: +[`commercial.md`](commercial.md). Support is email + GitHub issues; an in-app bug reporter is out +of product. -**The branding credit is OFF by default.** `MainViewModel.ShowBrandFlash` (internal static, default -`false`) is the master switch; `BrandFlashEnabled => ShowBrandFlash && !IsPremium` feeds the -presenter. The code is kept, not deleted — creator ruling: *"don't delete it, just make it -conditionally displayed."* `BrandFlashOutputTests` opts in by setting the switch before constructing -a `MainWindow`; `Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default. +**The branding credit is retired as a paid delta and stays off.** `MainViewModel.ShowBrandFlash` +(internal static, default `false`) is the master switch; `BrandFlashEnabled => ShowBrandFlash && +!IsPremium` feeds the presenter. The code is kept, not deleted — creator ruling: *"don't delete +it, just make it conditionally displayed."* Entitlement must never enable it. +`BrandFlashOutputTests` opts in by setting the switch before constructing a `MainWindow`; +`Credit_IsOffByDefault_EvenOnTheFreeTier` guards the default. If it is ever enabled, the mechanics that matter: @@ -117,14 +122,14 @@ If it is ever enabled, the mechanics that matter: - **A mid-credit downgrade** cuts the advertisement on the next tick: the presenter re-checks its own `Enabled` gate every frame rather than letting the credit finish. -Pricing, billing, the Store IAP plan and the rejected alternatives are history → -[`decisions.md`](decisions.md). +Live prices, entitlement and channels: [`commercial.md`](commercial.md). Retired pricing, the +Store IAP plan and the rejected alternatives: [`decisions.md`](decisions.md). **Monetization awareness (built-in, ungated, free — 2026-09-01; v1 SCOPE per the complete-v1 ruling — build order: capture → report → journey → Alerts):** the app is monetization-aware by -design, for **every** creator, free and ungated (the only subscriber swap remains the branding-flash -removal above). It has three layers, all free — this is the product's differentiator, not a paid tier: +design, for **every** creator, free and ungated. It has three layers, all free — the capability no +competitor ships (the positioning order across features lives in `MARCOM.md`): - **Reward-event capture (the data foundation).** The live-chat poll (`YouTubeChatService.Poll`) receives every reward event over `liveChat/messages` and — since TASK 43 (2026-09-24) — @@ -184,40 +189,32 @@ YouTube channel, they go to YouTube first. This assumption is explicit and must replaced by an in-app account-creation step. Zero state = the Start Stream dialog's "Sign in to YouTube" button; going live is unreachable until an account is connected. -## Windows packaging & distribution (direct + Steam; Store dropped 2026-10-22) +## Windows packaging & distribution (winget primary; Steam + Store parked) -**The Microsoft Store route is dead** — a brand on the publisher line needs a Company account and -a legal entity. Cause of death and the abandoned routes: [`decisions.md`](decisions.md). +Live channel policy, the artifact and signing costs: [`commercial.md`](commercial.md). Build +mechanics (publish flags, GitHub Releases, the winget manifest): [`Distribution.md`](../../Distribution.md). +Retired routes and cause of death: [`decisions.md`](decisions.md). -**The route now:** - -1. **Direct download from `llamacasty.com`** — the app is **free forever** (TASK 54), so there is - nothing to bill in v1. -2. **Steam when $100 exists** — Steam Direct is $100 per product (recoupable after $1,000 AGR), - accepted categories include *Audio/Video Production* / *Player Tools* (OBS Studio is the - precedent), the **developer/publisher name is self-chosen** so the brand needs no entity, - 5+ screenshots at 1920×1080 + capsule art, a build Valve runs, and a **2-week live page** - before release. Valve takes 30%. -3. **Polar is dormant, not deleted-in-principle** — chosen checkout/licence provider if a paid - tier ever returns; $50 already paid. `Distribution.md` §4.2/§4.5 holds the design. -4. **The Microsoft Store: not pursued.** The MSIX package stays as a working artifact and is - submitted nowhere. History: `TASKS/research-store-certification.md`, - `TASKS/task-48-distribution-msix.md`. +**The channels, in order:** direct download from `llamacasty.com` (binaries on GitHub Releases) → +**winget**, the primary install path → Steam (parked, $100, lifetime Pro as DLC only) → Microsoft +Store (parked, $19 Individual account — the only storefront that bills a subscription natively). **Consequences that are architecture, not packaging detail:** -- **⛔ The updater is now MISSING, not deleted-by-design.** Velopack and its bootstrap were - removed on 2026-10-22 (TASK 48 slice 3) because the Store was going to deliver updates — that - premise is gone. An update path must come back, and - `ytLive.Tests/StorePackagingTests.cs` (which asserts no updater returns) must be rewritten - with the chosen updater rather than left to veto one. -- **The MSIX package still builds** via `scripts/package-msix.sh` (TASK 48 slice 2) — publish +- **⛔ There is no self-updater, by policy as well as by history.** Velopack was removed on + 2026-10-22 (TASK 48 slice 3) because it downloaded *and executed*, which Store policy 10.2.2 + forbids — and the same rule would close the Store for good if it came back. Updates arrive + through the package manager (`winget upgrade`) plus an in-app "newer version exists" banner read + from a version file on GitHub Releases. `ytLive.Tests/StorePackagingTests.cs` must keep asserting + that no self-updater returns. +- **The MSIX package stays buildable** via `scripts/package-msix.sh` (TASK 48 slice 2) — publish `Release -r win-x64 --self-contained` into `artifacts/msix-stage`, copy `Packaging/Package.appxmanifest` in as `AppxManifest.xml` + `Packaging/Assets/`, then `makeappx pack` → `artifacts/LlamaCasty.msix` (159 MB, gitignored; `MSIX_PFX` signs it for a - local install). The manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with - `runFullTrust` + webcam + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in - `MyMistakes.md`. Logos come from the app's own artwork via `scripts/make-store-logos.py`. + local install). It is the Store channel's artifact, unsubmitted until that account exists. The + manifest is a **full-trust `packagedClassicApp` at `mediumIL`** with `runFullTrust` + webcam + + microphone. `Square310x310Logo`/`Wide150x150Logo` traps are in `MyMistakes.md`. Logos come from + the app's own artwork via `scripts/make-store-logos.py`. - **⛔ Bundle ffmpeg — no runtime downloads** (was Store policy 10.2.2; still right for a direct download, and it fixes the 2026-09-01 404). See [`output.md`](output.md) → "FFmpeg locator". - **⛔ 64-bit Windows only (creator ruling 2026-10-09).** Both csproj files set @@ -226,10 +223,12 @@ a legal entity. Cause of death and the abandoned routes: [`decisions.md`](decisi is the win64 build, ONNX Runtime 1.31 ships no win-x86 native (only win-x64/win-arm64), and a 32-bit address space can't hold the 1080p frame buffers. Windows 11 is 64-bit-only anyway. Public copy says "64-bit Windows 10 or 11". +- **Portable mode is the distributable's shape:** `portable.txt` beside the exe keeps the layout DB + and settings in `.\data\` (hung off `LayoutPathOverride`), so a USB stick carries someone's + scenes. The `winget` install and the hand-to-a-friend zip are the same artifact. -**What does NOT change: the monetization posture.** The app is **free forever with no watermark** -(TASK 54, 2026-10-08), so `IsPremium` gates nothing in v1 and the branding flash is OFF by -default. `Monetization` above governs. +**What does NOT change: no watermark and no feature gating.** The free tier is the whole app at a +720p30 profile. `Monetization` above governs. ### ⛔ Durable invariant: full trust, or recording silently breaks @@ -250,28 +249,24 @@ trust. No `broadFileSystemAccess` capability is needed, and none should be added 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 +### Signing — Azure Trusted Signing, never 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 2026-10-22 ruling -is to ship unsigned and accept the "unrecognized publisher" warning** until download reputation -accrues — buying a certificate is deferred, not rejected. `Distribution.md` §4.3 was corrected -2026-09-27 (it previously recommended EV). +Microsoft **removed the SmartScreen instant-bypass for EV certificates in March 2024**, so EV +($400+/yr) buys what OV (~$150–300/yr) buys; both need an HSM or hardware token and renew inside +**460 days**. The 2026-10-31 ruling is **Azure Trusted Signing (~$9.99/month)**: Microsoft-issued, +individuals eligible after identity verification, and it accrues reputation without a certificate +purchase. Until the profile exists, builds ship unsigned and the "unknown publisher" line is +accepted. Signing never clears the warning on day one either way — only download volume does. +Signing steps: [`Distribution.md`](../../Distribution.md). -### Distribution and licensing are independent — and the channel decision does not decide licensing +### Distribution and licensing are independent — the channel never decides billing A **Store-distributed** app *may* still verify licences with **Polar** (Store policy 10.8.1 explicitly permits a secure third-party purchase API for non-game PC products), so "which -channel?" never implied "which billing?" — route B was a real option. The 2026-10-22 reversal -proves the point in the other direction: dropping the Store did not by itself restart Polar, -because the **2026-10-08 free-forever ruling** (TASK 54) had already removed the paid tier. -Polar is therefore *dormant*, ready if a paid tier ever returns, and nothing in v1 depends on it. -The lesson worth keeping: the licensing choice rides on a **monetization** decision, not on a -distribution decision. +channel?" never implies "which billing?". **Polar is the live provider** (2026-10-31) because the +2026-10-30 freemium ruling created something to sell; winget has no storefront at all, so the +licence path is entirely ours there. The lesson worth keeping: the licensing choice rides on a +**monetization** decision, not on a distribution decision. ### ⛔ "Only when the user says so" is enforced by Windows, not by us