Integration Guide

Everything an integrator needs: the gate, activation, trials, enforcement, revocation, errors.

The two roles

 ┌──────────────────────┐   public key + license.lic   ┌──────────────────────┐
 │  ISSUER (you)        │ ───────────────────────────▶ │  CUSTOMER APP        │
 │  holds private key   │ ◀──────── HWID + payment ─── │  embeds public key   │
 └──────────────────────┘                              └──────────────────────┘

The private key never leaves your issuing machine. The customer app ships with only the public key — compiled in, pinned by fingerprint, never stored where it can be swapped.

LicenseGate — the whole API most apps need

gate = LicenseGate(
    app_name="MyProduct",            # per-machine namespace (must be unique per product)
    public_key=VENDOR_PUBLIC_KEY,    # embedded constant
    expected_fingerprint=FINGERPRINT,# startup self-check
    config_dir=cfg_dir,
    trial_days=14,
    enable_watchdog=True,            # background re-validation
    interval_seconds=600,
    on_violation=lambda reason, detail: shutdown(),
)

status = gate.start()      # startup: creates trial on first run
if not status:             # LicenseStatus is truthy when the app may run
    show_block_screen(status.message)

# later, after a purchase:
result = gate.activate_token(user_pasted_text)     # or gate.activate_file(path)
if result.ok:
    toast(f"Licensed to {result.payload.client}")

gate.check()               # re-resolve state any time
gate.status_summary()      # JSON-serializable dict for UIs / logs
gate.deactivate()          # archive license (falls back to trial/expired)

LicenseStatus fields

FieldMeaning
statelicensed · licensed_invalid · trial_active · trial_expired · tampered · missing · error
ok / bool()True when the app may run (licensed or trial active).
days_leftDays of validity or trial remaining.
clientCustomer name from the license payload.
messageHuman-readable sentence ready to display.
to_dict()JSON-safe summary for logs and support tickets.
Never-fall-back rule: if a license row exists but fails validation (tampered, expired, wrong machine), the gate reports licensed_invalid instead of quietly reverting to an active trial.

In-app activation (your UI, our validation)

# Method A — paste into YOUR text field:
result = gate.activate_token(text_entry.get().strip())

# Method B — file picker:
result = gate.activate_file(chosen_path)

Both validate signature → expiry → HWID → revocation → clock before storing. Rejections return canonical reasons your UI can map to messages: see the error table below.

Trials

Trial management is inside the gate. On first run gate.start() issues a self-signed, machine-bound trial license protected by the same clock guard as real licenses:

  • Editing the trial file → signature failure → tampered
  • Copying another machine's trial → HWID mismatch → tampered
  • Rolling back the clock → high-water mark → clock_tampering
  • Deleting the DB / trial data → start date ratcheted in redundant marks → trial does NOT restart

Revocation

# publish a signed list from your machine:
rizmi license revoke --license-id deploy-001 --private-key k.pem -o crl.json

# hand it to validators:
validator = LicenseValidator(pub_key_pem, revocation_list=open("crl.json").read())

A CRL with a bad signature is rejected loudly (revocation_list_invalid), never silently ignored.

Error codes

CodeCauseUser guidance
missingNo license found.Activate or buy.
expiredPast exp + grace_days.Renew.
hwid_mismatchBound to another machine.Re-issue for this machine's HWID.
tamperedSignature/row integrity failed.Re-install; contact support.
clock_tamperingClock rolled back/frozen.Fix system clock.
revokedID on a revocation list.Contact vendor.
decode_errorWrong key / corrupt token.Check public key matches issuer.
unsupported_schemaNewer schema than this build.Upgrade the library.

Grace period

Licenses carry grace_days: after exp, validation keeps succeeding and flags in_grace_period so your UI can warn ("renew within N days") while still enforcing eventually. Hard-stop happens at exp + grace_days.

Long-running processes

Servers and daemons expire while running. Enable the watchdog (enable_watchdog=True) so the gate re-validates every interval and fires on_violation(reason, detail) on expiry/tamper — your callback decides what "stop serving" means.

Multi-app machines

Every store is namespaced by app_name: separate directory, separate HMAC key, separate fallback files. Two py-rizmi apps cannot read or overwrite each other's state, and a DB copied between apps verifies under neither. Optionally share one clock ratchet across all your products (RizmiConfig(shared_clock_namespace=True)) so any app observing "tomorrow" protects them all.

Migrating old installs

rizmi migrate-to-sqlite run --config-dir ~/.config/MyApp --app-name MyApp

Idempotent; imports legacy trial key material so pre-existing trials keep their original dates.