#!/usr/bin/env python3 """Mint a signed license blob for a buyer. Creator-only tool. Signs with the Ed25519 private key from ``$DATATOOLS_LICENSE_PRIVKEY`` (production) or the in-tree dev key (local development). Every successful mint also appends a record to the issuance log at ``~/.datatools-creator/issued.jsonl`` (override with ``$DATATOOLS_ISSUANCE_LOG``). That log is the creator-side system of record for "who has a license" — useful for re-delivery, support, and as the seed for the future server-side ``licenses`` table. Examples -------- Mint a 1-year CORE license for Jane Doe:: python scripts/generate_license.py \\ --name "Jane Doe" --email jane@example.com --tier core Mint a 2-year PRO license and write the blob to a file:: python scripts/generate_license.py \\ --name "Acme Corp" --email ops@acme.com --tier pro \\ --years 2 --output acme.dtlic Mint with the production key (CI / manual fulfillment):: DATATOOLS_LICENSE_PRIVKEY= \\ python scripts/generate_license.py --name ... --email ... The output is a single base64-encoded token starting with ``DTLIC2:`` — paste this whole string into the buyer's delivery email or deliver as an attached ``.dtlic`` file. """ from __future__ import annotations import argparse import json import os import sys import uuid from pathlib import Path # Make ``src.license`` importable when run from the repo root. _PROJECT_ROOT = Path(__file__).resolve().parent.parent if str(_PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(_PROJECT_ROOT)) from src.license import Tier # noqa: E402 from src.license.crypto import encode_blob, sign # noqa: E402 from src.license.features import all_features_for_tier # noqa: E402 from src.license.schema import ( # noqa: E402 License, _utcnow_iso, default_expiry_iso, ) def default_issuance_log() -> Path: """Path to the local issuance log (creator-side ledger). Resolution order: 1. ``$DATATOOLS_ISSUANCE_LOG`` (absolute path; useful for tests and for pointing at a shared / encrypted volume). 2. ``~/.datatools-creator/issued.jsonl`` — separate from the buyer-facing ``~/.datatools/`` dir so it never gets bundled into a shipped install. """ override = os.environ.get("DATATOOLS_ISSUANCE_LOG") if override: return Path(override).expanduser().resolve() return Path.home() / ".datatools-creator" / "issued.jsonl" def append_issuance_log(record: dict, *, path: Path | None = None) -> Path | None: """Best-effort append of *record* to the issuance log. Returns the resolved path on success, ``None`` on IO failure (with a warning printed to stderr). We intentionally do not raise: the blob has already been minted by the time this runs, and losing one ledger row is strictly better than aborting after a successful mint and leaving the creator unsure whether to re-mint. """ p = path or default_issuance_log() try: p.parent.mkdir(parents=True, exist_ok=True) with p.open("a", encoding="utf-8") as f: f.write(json.dumps(record, sort_keys=True) + "\n") try: p.chmod(0o600) except OSError: pass return p except OSError as e: print( f"WARNING: could not write issuance log at {p}: {e}\n" " The blob above is still valid — record the mint " "manually.", file=sys.stderr, ) return None def build_args() -> argparse.ArgumentParser: p = argparse.ArgumentParser( description="Mint a signed DataTools license blob.", formatter_class=argparse.RawDescriptionHelpFormatter, ) p.add_argument("--name", required=True, help="Buyer's full name.") p.add_argument("--email", required=True, help="Buyer's email.") p.add_argument( "--tier", default=Tier.CORE.value, choices=[t.value for t in Tier], help="License tier (default: %(default)s).", ) p.add_argument( "--years", type=int, default=1, help="License lifetime in years (default: %(default)s).", ) p.add_argument( "--key", default=None, help="Override the auto-generated license key (default: random).", ) p.add_argument( "--output", "-o", type=Path, default=None, help="Write the blob to this file (default: print to stdout).", ) p.add_argument( "--no-log", action="store_true", help=( "Skip writing to the issuance log. Use for one-off test " "mints; do NOT use for real buyer fulfillment." ), ) return p def main(argv: list[str] | None = None) -> int: args = build_args().parse_args(argv) tier = Tier(args.tier) rid = uuid.uuid4().hex key = args.key or f"DT1-{tier.value.upper()}-{rid[:8]}-{rid[8:16]}" lic = License( name=args.name, email=args.email, license_key=key, tier=tier, features=all_features_for_tier(tier), issued_at=_utcnow_iso(), expires_at=default_expiry_iso(years=args.years), signature="", ) signature = sign(lic.to_canonical_dict()) payload = lic.to_canonical_dict() payload["signature"] = signature blob = encode_blob(payload) if not args.no_log: log_path = append_issuance_log({ "license_key": lic.license_key, "name": lic.name, "email": lic.email, "tier": lic.tier.value, "issued_at": lic.issued_at, "expires_at": lic.expires_at, "blob": blob, }) else: log_path = None if args.output: args.output.write_text(blob + "\n", encoding="utf-8") print(f"Wrote license to {args.output}", file=sys.stderr) else: print(blob) print( f" name: {lic.name}\n" f" email: {lic.email}\n" f" tier: {lic.tier.value}\n" f" key: {lic.license_key}\n" f" expires: {lic.expires_at}", file=sys.stderr, ) if log_path: print(f" logged: {log_path}", file=sys.stderr) return 0 if __name__ == "__main__": sys.exit(main())