## Offline Licensing

SignalReach supports an offline signed license file for tier enforcement.

### Runtime behavior

- If `SIGNALREACH_ENFORCE_LICENSE=1`, the app expects a signed `license.json`.
- If the signed license is valid, the payload tier (`lite`, `standard`, `pro`) becomes the install's licensed tier.
- If enforcement is enabled and the license is missing or invalid, the app falls back to `lite`.
- If enforcement is not enabled, the local/dev default remains `pro`.
- `SIGNALREACH_LICENSE_TIER` is a dev-only override and is ignored when enforcement is enabled.

### Files

- `license.json`: signed customer license payload shipped with the install
- `license_public.json`: public verification key shipped with the install
- `license_private.json`: private signing key kept only by the distributor

### Tooling

Generate a keypair once:

```powershell
.venv\Scripts\python.exe tools\license_tool.py generate-keypair --out-dir .\tmp\license_keys
```

Sign a customer license:

```powershell
.venv\Scripts\python.exe tools\license_tool.py sign-license `
  --private-key .\tmp\license_keys\license_private.json `
  --customer "Example Customer" `
  --tier standard `
  --license-id EXAMPLE-001 `
  --expires-at 2027-12-31 `
  --out .\tmp\license_keys\license.json
```

### Distribution

Ship these with the packaged app:

- signed `license.json`
- `license_public.json`
- launcher/config that sets `SIGNALREACH_ENFORCE_LICENSE=1`
- launcher/config that points `SIGNALREACH_LICENSE_FILE` and `SIGNALREACH_LICENSE_PUBLIC_KEY_FILE` at those packaged files if they do not live in the repo root

Do not ship `license_private.json`.

### Frozen package build

Build a signed frozen release folder:

```powershell
powershell -ExecutionPolicy Bypass -File .\tools\build_frozen_release.ps1 `
  -Tier standard `
  -Customer "Example Customer" `
  -LicenseId EXAMPLE-001 `
  -PrivateKey .\tmp\license_keys\license_private.json `
  -ExpiresAt 2027-12-31 `
  -Clean
```

Build a signed Windows installer:

```powershell
powershell -ExecutionPolicy Bypass -File .\tools\build_release_installer.ps1 `
  -Tier standard `
  -Customer "Example Customer" `
  -LicenseId EXAMPLE-001 `
  -PrivateKey .\tmp\license_keys\license_private.json `
  -ExpiresAt 2027-12-31 `
  -Clean
```

The frozen build:

- bundles Python, FastAPI, the UI, and planner code into a PyInstaller onedir app
- ships only the signed `license.json` and `license_public.json`
- stores runtime outputs and DEM cache under `%LOCALAPPDATA%\SignalReach`
- enables `SIGNALREACH_ENFORCE_LICENSE=1` automatically inside the frozen launcher

The local source checkout remains unlocked unless you explicitly set `SIGNALREACH_ENFORCE_LICENSE=1`.
For non-enforced local/dev work, you can still force a tier with `SIGNALREACH_LICENSE_TIER`.
