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>
5.5 KiB
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.
python scripts/generate_license.py \
--name "Michael Dombaugh" \
--email michael.dombaugh@gmail.com \
--tier core
Copy the DTLIC2:… blob from stdout, then activate:
python -m src.license_cli activate "DTLIC2:..." \
--name "Michael Dombaugh" \
--email michael.dombaugh@gmail.com
Verify:
python -m src.license_cli status
License lands at ~/.datatools/license.json, valid 1 year.
The
--name/activatemust 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.
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:
- Stash the private key in a password manager / KMS / hardware token. Losing it means no more renewals — see "Recovery" below.
- Delete
keypair.envfrom disk once stored. - Set the public key as
DATATOOLS_LICENSE_PUBKEYin 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:
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):
python scripts/generate_license.py --name "QA Bot" --email qa@datatools.app --tier core --years 5
Mint with a stable, human-readable key:
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):
python -m src.license_cli renew "DTLIC2:..."
Check what's active locally:
python -m src.license_cli status
Wipe a local license (move to a new machine, debug a buyer issue):
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:
- Generate a new keypair (
scripts/generate_keypair.py). - Ship a new build with the new public key.
- Re-issue every active buyer a new blob signed by the new private key.
- 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 |