From 65e17e0a701a6c7e79545b5cd2649e9a87461435 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 13 May 2026 22:10:16 +0000 Subject: [PATCH] docs(admin): internal license operations reference Creator-only ADMIN.md covering keypair generation, blob minting, dev vs. production key model, tier matrix, and recovery if the private key is lost. Includes a TL;DR for minting a dev license against the in-tree keypair. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/ADMIN.md | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 docs/ADMIN.md diff --git a/docs/ADMIN.md b/docs/ADMIN.md new file mode 100644 index 0000000..aedc344 --- /dev/null +++ b/docs/ADMIN.md @@ -0,0 +1,199 @@ +# 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 |