Files
datatools-dev/docs/ADMIN.md
Michael 65e17e0a70 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>
2026-05-13 22:10:16 +00:00

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 / --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.

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:

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:

  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