generate_license.py now appends every minted license to ~/.datatools-creator/issued.jsonl (overridable via env). This is the creator-side system of record until the server-side flow lands. The full blob is stored alongside name/email/tier/expiry so buyers who lose their delivery email can be re-served without re-minting. File is created mode 600 and lives outside the buyer-facing ~/.datatools/ dir so it never gets bundled into a shipped install. Log failures are non-fatal (warning to stderr) — the mint already succeeded by the time we try to log, and forcing a re-mint after a log error would invalidate any device the buyer had activated. Pass --no-log for test mints. ADMIN.md adds a "Customer record-keeping" section with the path, schema, jq one-liners, and migration note pointing at the upcoming LICENSE-SERVER.md design doc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7.9 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
Customer record-keeping — the issuance log
Every successful scripts/generate_license.py run appends one JSON
line to a local issuance log. This is the creator-side system of
record for "who has a license" until the server-side flow in
docs/LICENSE-SERVER.md lands.
Path: ~/.datatools-creator/issued.jsonl (override with
$DATATOOLS_ISSUANCE_LOG). Mode 600. Outside the buyer-facing
~/.datatools/ dir so it never gets bundled into a shipped install.
Format — one record per line:
{
"license_key": "DT1-CORE-5dd8e1db-d90c4656",
"name": "Michael Dombaugh",
"email": "michael.dombaugh@gmail.com",
"tier": "core",
"issued_at": "2026-05-13T22:10:27Z",
"expires_at": "2031-05-13T22:10:27Z",
"blob": "DTLIC2:..."
}
The full blob is stored so you can re-deliver to a buyer who lost their email without re-minting (the re-minted blob would have a different signature and would invalidate any device they'd already activated against the old one).
Useful operations:
# Full list of issued licenses
cat ~/.datatools-creator/issued.jsonl | jq
# Find by buyer email
jq -r 'select(.email == "buyer@example.com")' ~/.datatools-creator/issued.jsonl
# Count by tier
jq -r .tier ~/.datatools-creator/issued.jsonl | sort | uniq -c
# Licenses expiring in the next 30 days
jq -r 'select(.expires_at < "'"$(date -u -d '+30 days' +%Y-%m-%dT%H:%M:%SZ)"'") | .email' \
~/.datatools-creator/issued.jsonl
# Re-deliver a buyer's blob
jq -r 'select(.email == "buyer@example.com") | .blob' \
~/.datatools-creator/issued.jsonl
Skipping the log for test mints: pass --no-log. Never use this
for real buyer fulfillment — an unlogged mint is invisible to every
future query and to the eventual server-side migration.
Backup: treat this file like a small business ledger. Copy it into your password manager / encrypted cloud sync alongside the private key. Losing it doesn't break anything cryptographically (you can still mint new licenses) but it does lose the customer list.
Migrating to the server: the JSONL schema is intentionally close
to the planned licenses table in docs/LICENSE-SERVER.md. Once the
server is up, a one-shot import script will read the JSONL and
insert each row.
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 |
~/.datatools-creator/issued.jsonl |
Creator-side issuance log (one JSON line per mint) |
docs/LICENSE-SERVER.md |
Design for the future online issuance + record-keeping system |