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) <noreply@anthropic.com>
This commit is contained in:
2026-05-13 22:10:16 +00:00
parent e534fb4989
commit 65e17e0a70

199
docs/ADMIN.md Normal file
View File

@@ -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=<your-private-hex>
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 |