# ADMIN — Internal license operations Creator/operator-only reference. End users should read `USER-GUIDE.md` instead. This doc covers everything the creator does that buyers never see: generating the signing keypair, minting license blobs, the dev vs. production key story, and how to recover from key loss. --- ## TL;DR — I just need a license for my dev machine You're running from source, so the repo's embedded dev keypair signs and verifies. No env vars needed. ```bash python scripts/generate_license.py \ --name "Michael Dombaugh" \ --email michael.dombaugh@gmail.com \ --tier core ``` Copy the `DTLIC2:…` blob from stdout, then activate: ```bash python -m src.license_cli activate "DTLIC2:..." \ --name "Michael Dombaugh" \ --email michael.dombaugh@gmail.com ``` Verify: ```bash python -m src.license_cli status ``` License lands at `~/.datatools/license.json`, valid 1 year. > The `--name` / `--email` you pass to `activate` **must** match the values > the blob was minted with — they're part of the signed payload. --- ## Key model (Ed25519, asymmetric) | Key | Lives where | Used for | |-----|------------|---------| | **Private** (32 bytes hex) | Creator's password manager / KMS only | Signing license blobs | | **Public** (32 bytes hex) | Baked into the shipped binary | Verifying blobs at activation | The split is the whole point: an attacker with a copy of the binary still can't mint blobs — they'd need the private key, which never ships. There's also an in-tree **dev keypair** (`src/license/_dev_keypair.py`) derived deterministically from a seed. It's used when no env vars are set, so devs/tests can sign and verify locally without juggling secrets. Frozen builds that still use it are rejected at startup by `assert_production_safe` — see `src/license/crypto.py:84`. Blob format prefix: `DTLIC2:` (v1 was HMAC; v2 is Ed25519). --- ## One-time setup — generating the production keypair Run once, before the first paid release. ```bash python scripts/generate_keypair.py --output keypair.env ``` You'll get: ``` DATATOOLS_LICENSE_PRIVKEY=<64 hex chars> # KEEP SECRET DATATOOLS_LICENSE_PUBKEY=<64 hex chars> # BAKE INTO BUILD ``` Then: 1. **Stash the private key** in a password manager / KMS / hardware token. Losing it means no more renewals — see "Recovery" below. 2. **Delete `keypair.env`** from disk once stored. 3. **Set the public key** as `DATATOOLS_LICENSE_PUBKEY` in the PyInstaller build environment. The shipped binary embeds it via the env at freeze time. --- ## Minting a buyer license (production) With the production private key loaded: ```bash export DATATOOLS_LICENSE_PRIVKEY= python scripts/generate_license.py \ --name "Buyer Name" \ --email buyer@example.com \ --tier core \ --years 1 \ --output buyer.dtlic ``` Flags: | Flag | Default | Notes | |------|---------|-------| | `--name` | required | Buyer's full name. Goes into signed payload. | | `--email` | required | Buyer's email. Goes into signed payload. | | `--tier` | `core` | One of: `lite`, `core`, `pro` | | `--years` | `1` | Lifetime in years | | `--key` | random | Override the auto-generated license key | | `--output` / `-o` | stdout | Write blob to file instead of printing | Deliver the blob to the buyer either inline in the purchase email or as the attached `.dtlic` file. --- ## Tiers | Tier | Features | |------|---------| | **lite** | Deduplicator, Text Cleaner, Format Standardizer | | **core** | All 9 tools | | **pro** | All 9 tools + future Pro-only features | Source of truth: `src/license/features.py::all_features_for_tier`. --- ## Useful one-liners Mint a free internal/team license (dev key, no env needed): ```bash python scripts/generate_license.py --name "QA Bot" --email qa@datatools.app --tier core --years 5 ``` Mint with a stable, human-readable key: ```bash python scripts/generate_license.py --name "Acme Corp" --email ops@acme.com \ --tier pro --key "DT1-PRO-ACME-2026" ``` Renew an existing buyer (just re-mint with the same email; they paste the new blob): ```bash python -m src.license_cli renew "DTLIC2:..." ``` Check what's active locally: ```bash python -m src.license_cli status ``` Wipe a local license (move to a new machine, debug a buyer issue): ```bash python -m src.license_cli deactivate ``` --- ## Recovery — what if the private key is lost? Existing licenses keep working until they expire (the public key in the shipped binary still verifies them). What breaks: - **Renewals** — you can't mint a new blob for an existing buyer. - **New sales** — you can't mint anything. Path forward: 1. Generate a new keypair (`scripts/generate_keypair.py`). 2. Ship a new build with the new public key. 3. Re-issue every active buyer a new blob signed by the new private key. 4. Communicate the upgrade path to buyers. Treat the private key like a code-signing cert — back it up to two independent secure locations. --- ## Files & code pointers | Path | Purpose | |------|---------| | `scripts/generate_keypair.py` | One-time keypair generation | | `scripts/generate_license.py` | Mint a signed blob | | `src/license/crypto.py` | Sign / verify / dev-key detection | | `src/license/_dev_keypair.py` | In-tree dev keypair (never ships in prod) | | `src/license/manager.py` | `assert_production_safe` startup check | | `src/license/features.py` | Tier → features mapping | | `src/license_cli.py` | End-user `activate` / `status` / `renew` / `deactivate` | | `~/.datatools/license.json` | Where activated licenses are stored on each machine |