Files
datatools-dev/docs/ADMIN.md
Michael 23c51fd759 feat(license): local issuance log for minted blobs
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>
2026-05-13 22:25:19 +00:00

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

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:

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