Metadata-Version: 2.4
Name: keywarden
Version: 1.0.0
Summary: Official Python client for Key-Warden - validate software licences online (seat- and revocation-aware) or verify signed tokens offline against your embedded public key.
Author: Key-Warden
License: MIT
Project-URL: Homepage, https://key-warden.com
Project-URL: Documentation, https://key-warden.com/docs
Project-URL: Issues, https://key-warden.com/contact
Keywords: key-warden,keywarden,licence,license,licensing,activation,software-licensing,ed25519,offline-verification
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Security :: Cryptography
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: cryptography>=3.4
Dynamic: license-file

# keywarden

The official Python client for [Key-Warden](https://key-warden.com). Validate a
software licence online — seat-aware, revocation-aware — or verify a signed token
offline against your embedded public key, with no network round-trip.

One dependency: [`cryptography`](https://pypi.org/project/cryptography/) (for
Ed25519). Networking is stdlib `urllib`. Python 3.8+.

```bash
pip install keywarden
```

## Validate online

The authoritative check. Ask the platform whether a licence is good *right now*.

```python
import os, socket
import keywarden as kw

res = kw.validate(
    customer_licence_key,
    apim_key=os.environ["KW_APIM_KEY"],      # your APIM subscription key
    client_key=os.environ["KW_CLIENT_KEY"],  # your validation key
    machine_id=kw.machine_id_from(socket.gethostname(), user_id),  # stable, hashed your side
)

if not res["valid"]:
    raise SystemExit(f"licence not valid: {res.get('reason')}")
# res["token"] is a freshly signed proof — cache it for the offline path below.
```

A `valid == False` (e.g. `revoked`, `expired`, `seat_limit_exceeded`) is **data**,
not an error. A wrong `client_key` raises a `KeyWardenError` with
`code == "unauthorized_client"` — that's *your* auth failing, and your customer
should never see it as a licence problem.

## Verify offline

No connection? Verify a token you already hold against your **public** key —
the 32-byte raw key from your vendor console. Pure, no network.

```python
check = kw.verify_token(cached_token, os.environ["KW_PUBLIC_KEY"])
if not check["valid"]:
    lock_features(check["reason"])  # "bad_signature" | "expired" | ...
```

The token is `header.body.signature` (compact JWT style) and the Ed25519
signature covers the exact bytes `header.body`. This client verifies over those
bytes — it never decodes-then-reverifies, which is the one mistake that silently
breaks offline checks. Expiry is honoured within the offline grace window you set
at mint time.

## Online, with an offline fallback

The pattern most desktop apps want: online is authoritative; if the network is
down, keep working within grace.

```python
res = kw.validate_or_verify(
    customer_licence_key,
    apim_key=apim_key, client_key=client_key, machine_id=machine_id,
    cached_token=last_good_token,            # from a previous validate()
    public_key=os.environ["KW_PUBLIC_KEY"],
)
# res["source"] == "online" | "offline"
```

A rejected `client_key` (401) is never masked by the offline path — only a genuine
reachability failure falls back.

## API

| Function | Purpose |
|---|---|
| `validate(key, *, apim_key, client_key, ...)` | Online check. Returns `{"valid", "reason"?, "activeSeats"?, "token"?}`. |
| `verify_token(token, raw_pub_b64, *, now=None)` | Offline check. Returns `{"valid", "reason"?, "claims"?}`. |
| `validate_or_verify(key, *, cached_token, public_key, ...)` | Online, falling back to a cached token when unreachable. |
| `machine_id_from(*parts)` | A stable SHA-256 machine id; raw parts never leave the machine. |

Any real failure (bad credentials, unreachable gateway, server error) raises
`KeyWardenError`, which carries `.code` and `.status`.

## Security notes

- Your **private** signing key never leaves Key-Warden's Key Vault. You embed
  only the 32-byte public half.
- `machine_id` is hashed by the platform, but send an opaque, stable id — not a
  raw MAC address or a hostname you wouldn't want logged. `machine_id_from()`
  hashes on your side too.
- Two independent credentials gate every online call: the APIM subscription key
  gets you to the gateway, the validation key authenticates you as the vendor. A
  leaked validation key can be rotated without reissuing a single customer
  licence.

## Licence

MIT.
