Metadata-Version: 2.4
Name: licentric
Version: 0.3.0
Summary: Python SDK for Licentric — license keys, machine activation, offline validation, AI agent monetization, and Stripe-driven license provisioning. Alternative to Keygen, Cryptlex, LemonSqueezy License Keys.
Project-URL: Homepage, https://licentric.com
Project-URL: Documentation, https://licentric.com/docs
Project-URL: Repository, https://github.com/AbleVarghese/licentric
Project-URL: Changelog, https://github.com/AbleVarghese/licentric/blob/main/CHANGELOG.md
Project-URL: Bug Tracker, https://github.com/AbleVarghese/licentric/issues
Author: Able Varghese
License-Expression: MIT
License-File: LICENSE
Keywords: agent-monetization,ai-agent,api,compliance,consumption-billing,cryptlex-alternative,drm,entitlements,eu-ai-act,feature-flags,gdpr,indie-developer,keygen-alternative,lemonsqueezy-alternative,license-key,license-management,license-server,license-validation,licensing,licentric,machine-activation,mcp,mcp-server,metering,monetization,offline-validation,saas,sdk,software-licensing,stripe,stripe-integration,subscription,token-budget,usage-metering
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development
Classifier: Topic :: Software Development :: Libraries
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: System :: Software Distribution
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.0
Provides-Extra: dev
Requires-Dist: cryptography>=42.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Provides-Extra: offline
Requires-Dist: cryptography>=42.0; extra == 'offline'
Description-Content-Type: text/markdown

# Licentric Python SDK

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Official Python client for the [Licentric](https://licentric.com) software licensing API. Validate licenses, manage machine activations, send heartbeats, and check out offline license files.

## Installation

```bash
pip install licentric
```

**Requirements:** Python 3.9+

### Install from source (for contributors)

```bash
cd sdks/python
pip install -e .
```

## Quick Start

```python
from licentric import Licentric

client = Licentric(api_key="lk_live_...")

# Validate a license key
result = client.validate(key="LIC-XXXX-XXXX-XXXX")
if result.valid:
    print(f"License is valid — code: {result.code}")
```

## Machine Activation

```python
from licentric import Licentric
from licentric.fingerprint import fingerprint

client = Licentric(api_key="lk_live_...")

# Activate a machine
machine = client.activate(
    key="LIC-XXXX-XXXX-XXXX",
    fingerprint=fingerprint(),
    name="production-server",
    platform="linux",
)
print(f"Activated machine: {machine.id}")

# Send heartbeat to keep the machine alive
client.heartbeat(machine_id=machine.id, key="LIC-XXXX-XXXX-XXXX")

# Deactivate when done
client.deactivate(machine_id=machine.id, key="LIC-XXXX-XXXX-XXXX")
```

## Fetching License Details

`get_license` and `get_license_by_key` return the full license — including
all activated machines and the bound policy snapshot — in a single API call.
Useful for dashboards, admin UIs, and any flow that needs to inspect the
ruleset without making three sequential requests.

```python
client = Licentric(api_key="lk_live_...")

# By internal license ID
license = client.get_license("lic_abc123")

# Or by raw license key (URL-encoded server-side)
license = client.get_license_by_key("LIC-XXXX-XXXX-XXXX-XXXX")

print(f"Status: {license.status}")
print(f"Active machines: {len(license.machines or [])}")
print(f"Max machines allowed: {license.policy.max_machines}")
print(f"Offline allowed: {license.policy.offline_allowed}")
print(f"Feature flags: {license.policy.metadata.get('feature_flags', [])}")
```

The raw `key` and `key_hash` fields are stripped server-side — `License`
returned here never contains the secret material.

## Offline License Files

Check out a signed license file for offline validation:

```python
license_file = client.checkout(
    license_id="uuid-of-license",
    fingerprint=fingerprint(),
    ttl=1209600,  # 14 days
)
print(f"Certificate: {license_file.certificate}")
```

## Offline Caching

Cache validation results locally with HMAC-SHA256 tamper detection:

```python
from licentric.cache import ValidationCache

cache = ValidationCache()

# Store a validation result (default TTL: 15 minutes)
cache.set(key_hash="sha256_of_license_key", result={"valid": True, "code": "VALID"})

# Retrieve (returns None if expired or tampered)
cached = cache.get(key_hash="sha256_of_license_key")
if cached:
    print(cached.data)
```

## Response Signature Verification

The Licentric backend signs validation responses with HMAC-SHA256 over the
raw response body. The SDK verifies the `X-Signature` and
`X-Signature-Timestamp` headers before trusting the result, defeating MITM
attackers who would otherwise flip `valid: False` -> `valid: True` in flight.

```python
from licentric import Licentric, SignatureVerificationError

client = Licentric(
    api_key="lk_live_...",
    # Returned alongside `plain_key` by POST /api/dashboard/api-keys.
    # Same value for every API key in the account; show it to the user once.
    # IMPORTANT: this is the **response** signing key (HMAC over validation
    # responses). It is NOT the same as the webhook signing secret
    # (`whsec_...`) returned when you create a webhook endpoint. Passing a
    # `whsec_...` value here will trigger a startup WARNING and break every
    # signature check.
    signing_key="abcd1234...",  # hex, 32 bytes
)

try:
    result = client.validate(key="LIC-XXXX")
except SignatureVerificationError:
    # Hard failure — the response was tampered with, replayed, or unsigned.
    # Never grace-eligible: a forged `valid=True` is worse than refusing
    # to validate. The SDK refuses to write unverified responses to the cache.
    ...
```

**Defaults.** When `signing_key` is supplied, verification runs automatically.
When it is not, the SDK skips verification and logs a `WARNING` —
backward-compatible for clients that haven't yet upgraded their key handling
but explicitly noisy so the gap is visible. To disable verification while
your `signing_key` is configured (rarely needed), pass `verify_signature=False`.

**Replay protection.** Responses with `X-Signature-Timestamp` more than 5
minutes off the local clock are rejected.

**Cache integrity.** A failed signature verification short-circuits before
the cache write, so a forged response cannot poison the offline cache.

## Error Handling

All API errors inherit from `LicentricError` with structured fields:

```python
from licentric import (
    Licentric,
    AuthenticationError,
    LicenseRevokedError,
    LicenseSuspendedError,
    LicenseExpiredError,
    NotFoundError,
    RateLimitError,
)
import time

client = Licentric(api_key="lk_live_...")

try:
    machine = client.activate(key="LIC-XXXX", fingerprint="abc123")
except LicenseRevokedError:
    # Permanent — show "license has been revoked, contact support"
    print("License revoked")
except LicenseSuspendedError:
    # Temporary — usually a payment issue; surface "billing on hold"
    print("License suspended")
except LicenseExpiredError:
    # Renewal flow
    print("License expired — please renew")
except RateLimitError as e:
    # Retry-eligible after backoff
    time.sleep(e.retry_after)
except AuthenticationError:
    # Configuration problem — fail immediately
    print("Invalid API key")
```

### HTTP-status exceptions (raised by HTTP code)

| Exception | HTTP Status | When |
|-----------|-------------|------|
| `AuthenticationError` | 401 | Invalid or missing API key |
| `ForbiddenError` | 403 | Authenticated but lacks permission/scope |
| `NotFoundError` | 404 | Resource does not exist |
| `ValidationError` | 400 | Invalid request parameters (`details` has Zod issues) |
| `ConflictError` | 409 | Generic state conflict (no specific license-state code) |
| `RateLimitError` | 429 | Too many requests (use `retry_after`) |
| `LicentricError` | 5xx + other | Base class — also raised for unknown statuses |

### License-state exceptions (raised by body `code`, regardless of HTTP status)

These subclass `LicentricError` directly — NOT the HTTP-status classes — so
catching `ConflictError` will NOT catch them. Catch by license-state class
explicitly:

| Exception | API code | When |
|-----------|----------|------|
| `LicenseRevokedError` | `LICENSE_REVOKED` | License has been revoked (permanent) |
| `LicenseSuspendedError` | `LICENSE_SUSPENDED` | License is suspended (e.g. payment failure — temporary) |
| `LicenseExpiredError` | `LICENSE_EXPIRED` | License past expiration and policy disallows continued use |
| `LicenseBannedError` | `LICENSE_BANNED` | License banned for abuse (terminal — no recovery) |
| `SignatureVerificationError` | `SIGNATURE_INVALID` | Response signature failed HMAC check (MITM, replay, or stripped header) — never grace-eligible |

### Recovery classification — what to retry vs fail immediately

| Strategy | Exceptions |
|----------|------------|
| **Grace-eligible** (use cached validation if available, retry on next request) | Network errors, 5xx `LicentricError`, `RateLimitError` after backoff completes |
| **Fail immediately** (do not retry; surface to end user) | `LicenseRevokedError`, `LicenseSuspendedError`, `LicenseExpiredError`, `LicenseBannedError`, `AuthenticationError`, `NotFoundError`, `ValidationError`, `SignatureVerificationError` |

## API Reference

### `Licentric(api_key, base_url?, timeout?)`

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | `str` | — | Your API key (`lk_live_...` or `lk_test_...`) |
| `base_url` | `str` | `https://licentric.com` | API base URL |
| `timeout` | `float` | `30.0` | Request timeout in seconds |

### Methods

| Method | Auth | Description |
|--------|------|-------------|
| `validate(key, fingerprint?, entitlements?)` | None (public) | Validate a license key (key passed in body, no auth header) |
| `activate(key, fingerprint, name?, platform?)` | License key | Activate a machine |
| `deactivate(machine_id, key)` | License key | Remove a machine activation |
| `heartbeat(machine_id, key)` | License key | Send a machine heartbeat |
| `checkout(license_id, fingerprint, ttl?)` | API key | Check out an offline license file |
| `close()` | — | Close the HTTP client |

The client supports context manager usage:

```python
with Licentric(api_key="lk_live_...") as client:
    result = client.validate(key="LIC-XXXX-XXXX-XXXX")
```

## Machine Fingerprinting

The `fingerprint()` function generates a stable SHA-256 hash identifying the current machine:

| Platform | Source |
|----------|--------|
| macOS | Hardware UUID via `ioreg` |
| Linux | `/etc/machine-id` or Docker container ID |
| Windows | Product UUID via `wmic` |
| Fallback | `hostname:username` hash |

## Type Safety

This package includes a `py.typed` marker ([PEP 561](https://peps.python.org/pep-0561/)) and ships with full type annotations. Works with mypy, pyright, and other type checkers out of the box.

## Documentation

- [Getting Started Guide](https://licentric.com/docs/getting-started/quickstart)
- [Python SDK Reference](https://licentric.com/docs/sdks/python)
- [API Reference](https://licentric.com/api/reference)

## License

MIT
