|
| 1 | +# Off Grid AI Desktop — Release Runbook |
| 2 | + |
| 3 | +How we ship Off Grid AI Desktop: two artifacts (**core** = free, **pro** = |
| 4 | +license-gated), signed + notarized, with Keygen license activation. Mirrors the |
| 5 | +mobile licensing model (same Keygen account/product — a key works on both). |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 0. Mental model |
| 10 | + |
| 11 | +- **Core build** — no `pro/` submodule code bundled (`__OFFGRID_PRO__ = false`). |
| 12 | + Free features only. Pro tabs show the UpgradeScreen. |
| 13 | +- **Pro build** — `pro/` submodule bundled (`__OFFGRID_PRO__ = true`), but pro |
| 14 | + stays **locked until a valid Keygen license is activated** at runtime. A leaked |
| 15 | + pro DMG is inert without a key. |
| 16 | +- The gate lives in the **main process** (`src/main/bootstrap/loadProFeaturesMain.ts` |
| 17 | + → `proEnabled()`), which consults `isProEntitled()` from |
| 18 | + `src/main/licensing/license-service.ts`. The renderer reads the verdict |
| 19 | + synchronously at load via the `pro:is-enabled` IPC (preload → `window.api.isPro`). |
| 20 | + |
| 21 | +Env overrides (dev/contributor only): |
| 22 | +- `OFFGRID_PRO=0` → force free even in a pro build. |
| 23 | +- `OFFGRID_PRO=1` → force pro on **without** a license (for working on pro features). |
| 24 | +- unset → the real paid path: license-gated. |
| 25 | +- `OFFGRID_FORCE_CORE=1` (build-time) → build a core artifact even though `pro/` |
| 26 | + is checked out (used to produce both DMGs from one checkout). |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## 1. Local test build (no signing, no publish) ← do this BEFORE any release |
| 31 | + |
| 32 | +The last macOS release published but didn't run properly. Always smoke-test a |
| 33 | +**packaged** build locally first. |
| 34 | + |
| 35 | +```bash |
| 36 | +git lfs pull # ensure resources/bin/* are real binaries, not LFS stubs |
| 37 | +./scripts/build-mac-local.sh both # → dist/OffGrid-core-<v>.dmg + dist/OffGrid-pro-<v>.dmg |
| 38 | +# or: ./scripts/build-mac-local.sh core / pro |
| 39 | +``` |
| 40 | + |
| 41 | +These builds are **unsigned** (no cert/Apple-ID prompts) and never touch GitHub. |
| 42 | + |
| 43 | +### Smoke test |
| 44 | +1. Open `dist/OffGrid-core-<v>.dmg`, drag to /Applications, **right-click → Open** |
| 45 | + (unsigned → Gatekeeper). Confirm: app launches, a model downloads + chat works, |
| 46 | + pro tabs show the UpgradeScreen (no license box, since core). |
| 47 | +2. Open `dist/OffGrid-pro-<v>.dmg` similarly. Confirm: app launches, pro tabs show |
| 48 | + the UpgradeScreen **with** a "Enter your license key" box. Paste a valid key → |
| 49 | + "Activated. Restart to finish unlocking Pro." → Restart → pro tabs unlock. |
| 50 | +3. Things that broke before — verify explicitly: local model server starts on |
| 51 | + :7878 (chat responds), whisper/ffmpeg present, no missing-binary errors in the |
| 52 | + console (`Console.app` → filter "Off Grid"). |
| 53 | + |
| 54 | +> Both DMGs use distinct appIds (`…desktop` / `…desktop.pro`) so they can be |
| 55 | +> installed side by side. They share the canonical userData dir |
| 56 | +> (`~/Library/Application Support/Off Grid AI Desktop`), so an activated license |
| 57 | +> persists across both. |
| 58 | +
|
| 59 | +--- |
| 60 | + |
| 61 | +## 2. Licensing — how a purchase becomes an unlock |
| 62 | + |
| 63 | +This backend already exists (shared with mobile). Nothing to build: |
| 64 | + |
| 65 | +``` |
| 66 | +Buy Pro on web (RevenueCat checkout) |
| 67 | + → RevenueCat webhook → Cloudflare Worker (license.getoffgridai.co) |
| 68 | + → Worker creates/renews a Keygen license, emails the key via Resend |
| 69 | + → user pastes key into the app (UpgradeScreen) |
| 70 | + → app validates via Keygen + activates this machine (platform tag macos/windows) |
| 71 | + → entitlement cached, encrypted, in userData/license.json |
| 72 | + → background revalidation catches expiry/revocation when online |
| 73 | +``` |
| 74 | + |
| 75 | +Desktop licensing code (ported from mobile): |
| 76 | +- `src/main/licensing/keygen-config.ts` — account/product/policy IDs, public key (non-secret). |
| 77 | +- `src/main/licensing/keygen-client.ts` — Keygen REST (validate / activate / list / deactivate). |
| 78 | +- `src/main/licensing/device-fingerprint.ts` — stable per-install id (userData file). |
| 79 | +- `src/main/licensing/license-service.ts` — entitlement logic, encrypted cache, revalidation. |
| 80 | +- `src/main/license-ipc.ts` — IPC: `pro:is-enabled` (sync), `license:activate|status|…`. |
| 81 | + |
| 82 | +Device cap is enforced server-side by Keygen. Reinstalls reuse the same |
| 83 | +fingerprint and reclaim their slot. |
| 84 | + |
| 85 | +--- |
| 86 | + |
| 87 | +## 3. Real release (signed + notarized, two artifacts) |
| 88 | + |
| 89 | +> Not yet wired into CI as two artifacts — see TODO at the bottom. The mechanics: |
| 90 | +
|
| 91 | +### macOS (working today) |
| 92 | +Signing uses the Apple Developer ID + notarization (`electron-builder.yml` |
| 93 | +`mac.notarize: true`). CI secrets: `CSC_LINK`, `CSC_KEY_PASSWORD`, |
| 94 | +`APPLE_API_KEY*`. Do **not** add an afterSign re-sign hook — it invalidates the |
| 95 | +notarization staple (this was a past bug). |
| 96 | + |
| 97 | +### Windows — PARKED |
| 98 | +Windows is on hold and owned elsewhere (the bundled llama-server doesn't start in |
| 99 | +the Windows binary; being fixed separately). Azure Trusted Signing setup (§4) is |
| 100 | +kept below for when Windows resumes, but it is **not** on the current path. Focus |
| 101 | +is macOS core + pro. |
| 102 | + |
| 103 | +### Per-artifact config |
| 104 | +Core and pro differ only by `OFFGRID_FORCE_CORE`, `productName`, `appId`, and |
| 105 | +`artifactName` (see `scripts/build-mac-local.sh` for the exact overrides). They |
| 106 | +must publish to **separate update channels** so electron-updater never feeds a |
| 107 | +core user a pro binary (or vice versa). |
| 108 | + |
| 109 | +--- |
| 110 | + |
| 111 | +## 4. Azure Trusted Signing setup (Windows) — PARKED (kept for when Windows resumes) |
| 112 | + |
| 113 | +Modern Windows code-signing requires the private key on certified hardware/HSM, |
| 114 | +so a downloadable `.pfx` no longer works in CI. Azure Trusted Signing is the |
| 115 | +cheapest CI-native option (~$10/mo) and electron-builder 25+ supports it natively. |
| 116 | + |
| 117 | +### One-time setup (Azure portal — only the org owner can do this) |
| 118 | +1. **Subscription + provider**: in an Azure subscription, register the |
| 119 | + `Microsoft.CodeSigning` resource provider. |
| 120 | +2. **Trusted Signing Account**: create one (region: East US / West US3 / |
| 121 | + West Central US / North or West Europe). |
| 122 | +3. **Identity Validation** ⏳ **(1–3 weeks — start FIRST)**: submit legal business |
| 123 | + name + D-U-N-S + address. The validated name becomes the cert subject and your |
| 124 | + `publisherName`. |
| 125 | +4. **Certificate Profile**: once validation passes, create a **Public Trust** profile. |
| 126 | +5. **CI service principal**: create an App Registration + client secret, and assign |
| 127 | + it the **"Trusted Signing Certificate Profile Signer"** role on the account. |
| 128 | + |
| 129 | +### Values to collect |
| 130 | +| What | Where it goes | |
| 131 | +|------|----------------| |
| 132 | +| `endpoint` (e.g. `https://eus.codesigning.azure.net/`) | electron-builder config | |
| 133 | +| `codeSigningAccountName` | electron-builder config | |
| 134 | +| `certificateProfileName` | electron-builder config | |
| 135 | +| `publisherName` (exact validated org name) | electron-builder config | |
| 136 | +| `AZURE_TENANT_ID` | GitHub repo secret | |
| 137 | +| `AZURE_CLIENT_ID` | GitHub repo secret | |
| 138 | +| `AZURE_CLIENT_SECRET` | GitHub repo secret | |
| 139 | + |
| 140 | +### electron-builder wiring (when values are in hand) |
| 141 | +Add to the Windows config (electron-builder 25+ reads `AZURE_*` env via |
| 142 | +DefaultAzureCredential and auto-downloads the Trusted Signing dlib): |
| 143 | + |
| 144 | +```yaml |
| 145 | +win: |
| 146 | + executableName: off-grid-ai |
| 147 | + azureSignOptions: |
| 148 | + publisherName: "<validated org name>" |
| 149 | + endpoint: "https://<region>.codesigning.azure.net/" |
| 150 | + certificateProfileName: "<profile>" |
| 151 | + codeSigningAccountName: "<account>" |
| 152 | +``` |
| 153 | +
|
| 154 | +SmartScreen reputation accrues over downloads; Microsoft-backed certs gain trust |
| 155 | +quickly. No EV needed. |
| 156 | +
|
| 157 | +--- |
| 158 | +
|
| 159 | +## TODO before first paid release (macOS) |
| 160 | +- [ ] Split `.github/workflows/release.yml` into **macOS** core + pro build jobs, |
| 161 | + using `OFFGRID_FORCE_CORE` and the per-artifact overrides; separate update channels. |
| 162 | +- [ ] Confirm the RevenueCat offering/products issue desktop-valid keys (they're |
| 163 | + product-scoped, so likely already do) and the key email copy isn't mobile-only. |
| 164 | +- [ ] Device-management UI (list/deactivate machines) — backend is ready |
| 165 | + (`license:list-devices` / `license:deactivate`). |
| 166 | + |
| 167 | +### Parked (owned elsewhere) |
| 168 | +- [ ] Windows: bundled llama-server doesn't start — fix in progress separately. |
| 169 | +- [ ] Azure Trusted Signing validated + `azureSignOptions` wired (§4) — resumes with Windows. |
0 commit comments