Metadata-Version: 2.4
Name: authlix
Version: 0.5.0
Summary: Client and manager helpers for Authlix
Author-email: Authlix <me@showdown.boo>
License: MIT License
        
        Copyright (c) 2026 Authlix
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://authlix.io/
Classifier: License :: OSI Approved :: MIT License
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
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.31.0
Requires-Dist: PyNaCl>=1.5.0
Dynamic: license-file

# Authlix Python SDK

[![PyPI version](https://img.shields.io/pypi/v/authlix?color=blue&logo=pypi&logoColor=white)](https://pypi.org/project/authlix/)
[![Python](https://img.shields.io/pypi/pyversions/authlix?logo=python&logoColor=white)](https://pypi.org/project/authlix/)
[![Downloads](https://img.shields.io/pypi/dm/authlix?color=green)](https://pypi.org/project/authlix/)
[![License](https://img.shields.io/pypi/l/authlix)](https://pypi.org/project/authlix/)
[![Docs](https://img.shields.io/badge/docs-authlix.io-blue?logo=readthedocs)](https://authlix.io/)

> Client and manager helpers for integrating public validation flows and project operations with the Authlix API.

**License client** – validates license keys, collects environment metadata and lets end-users update `client_editable` fields.
**Manager client** – handles authentication plus CRUD operations for projects, metadata schemas, licenses and API keys.
**Cryptographic signing** – Ed25519 response signatures, SealedBox HWID encryption and anti-replay protection.
**Environment helpers** – gather HWID, local/public IPs and other contextual information.

The SDK targets Python 3.9+ and is published to PyPI as `authlix`.

## Installation

```bash
pip install authlix
```

PyNaCl is included as a dependency and is required for the cryptographic signing features.

## Quick start

### Validate a license key

```python
from authlix import LicenseClient

client = LicenseClient(
    base_url="https://authlix.io",
    project_id=42,
)

# Automatically collects HWID as machine fingerprint
result = client.validate_with_environment("CL-XXXX-XXXX")
print(result)
# {"valid": True, "expires_at": "2026-12-31T00:00:00", "metadata": {...}}
```

To control the payload manually, call `validate_license` and pass `hwid`, `ip`, and `metadata` yourself. The `metadata` must only contain fields that are marked `client_editable` in your project schema.

### Validate with cryptographic security

When your project has crypto enabled, the SDK can encrypt the HWID, add anti-replay parameters and automatically verify the Ed25519 signature on every response:

```python
from authlix import LicenseClient

client = LicenseClient(
    base_url="https://authlix.io",
    project_id=42,
)

# Fetch and cache the project's Ed25519 public key (once per session)
client.fetch_public_key()

# HWID is now encrypted with SealedBox; the response signature is auto-verified
result = client.validate_with_environment("CL-XXXX-XXXX")
```

Or embed the public key directly to avoid the extra round-trip:

```python
client = LicenseClient(
    base_url="https://authlix.io",
    project_id=42,
    public_key="<hex-encoded-ed25519-public-key>",
)
```

`LicenseClient` constructor options:

| Parameter | Default | Description |
|---|---|---|
| `public_key` | `None` | Hex Ed25519 public key. Enables HWID encryption and signature verification. |
| `auto_verify` | `True` | Verify Ed25519 signature on every response (requires `public_key`). |
| `anti_replay` | `True` | Include `timestamp` + `nonce` in requests (requires `public_key`). |

### Manage projects and licenses

```python
from authlix import ManagerClient

manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("automation_bot", api_key="sk_live_XXXXXXXXXXXXXXXX")

# Create a project
project = manager.create_project(
    name="My Awesome App",
    description="Tooling pour la communauté",
)

# Create a 30-day license
license_data = manager.create_license(
    project_id=project["id"],
    days_valid=30,
    metadata={"plan": "pro"},
)

# Revoke a license
manager.update_license(
    license_id=license_data["id"],
    is_active=False,
    metadata={"plan": "revoked", "notes": "Chargeback"},
)

# Extend expiration by 14 days
manager.extend_license(project_id=project["id"], license_id=license_data["id"], days=14)

# Delegate day-to-day operations to a teammate
manager.add_project_manager(project_id=project["id"], username="support_team")
```

### Look up a license by key

Prefer `get_license_by_key` over fetching the full list when you only need one license:

```python
from authlix import ManagerClient, NotFoundError, ForbiddenError

manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("admin", api_key="sk_live_XXXXXXXXXXXXXXXX")

try:
    lic = manager.get_license_by_key(project_id=42, license_key="CL-XXXX-XXXX")
    print(f"ID: {lic['id']}  active: {lic['is_active']}")
except NotFoundError:
    print("Key not found in this project")
except ForbiddenError:
    print("Insufficient permissions")
```

The key is automatically normalized to uppercase before lookup.

### API key management with scopes

```python
from authlix import ManagerClient, Scopes

manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("admin", api_key="sk_live_XXXXXXXXXXXXXXXX")

# Password auth is also supported; in production include hCaptcha token when required.
# manager.authenticate("admin", password="password", hcaptcha_token="<token>")

# Create a key scoped to one project with granular permissions
api_key = manager.create_api_key(
    label="CI Runner",
    project_ids=[project["id"]],
    scopes=[Scopes.LICENSES_READ, Scopes.LICENSES_WRITE],
)

# Expand its scope later
manager.update_api_key(api_key_id=api_key["id"], project_ids=[project["id"], 7])

# Operator-wide audit view
manager.list_api_keys()

# Per-project view
manager.list_project_api_keys(project_id=project["id"])

# Retire when done
manager.delete_api_key(api_key_id=api_key["id"])
```

Available scopes:

| Constant | Description |
|---|---|
| `Scopes.LICENSES_READ` | Read license data |
| `Scopes.LICENSES_WRITE` | Create / update / delete licenses |
| `Scopes.PROJECTS_READ` | Read project settings |
| `Scopes.PROJECTS_WRITE` | Modify project settings |
| `Scopes.API_KEYS_MANAGE` | Create / revoke API keys |
| `Scopes.WEBHOOKS_MANAGE` | Manage webhook endpoints |
| `Scopes.AUDIT_READ` | Read audit logs |
| `Scopes.OFFLINE_MANAGE` | Issue / revoke offline tokens |
| `Scopes.QUOTA_READ` | Read usage and quota data |
| `Scopes.ALL` | All scopes (list) |

Legacy keys created before scopes were introduced automatically have full access.

## Client-editable metadata

Certain metadata fields can be safely mutated by end-users via the `client_editable` flag.

**1. Manager defines the schema:**

```python
manager.update_metadata_schema(
    project_id=42,
    fields=[
        {"key": "notes", "type": "string", "client_editable": True},
        {"key": "plan",  "type": "string", "client_editable": False},
    ],
)
```

**2. Client updates the editable fields:**

```python
from authlix import LicenseClient

client = LicenseClient(base_url="https://authlix.io", project_id=42)
client.update_client_metadata(
    key="CL-XXXX-XXXX",
    metadata={"notes": "Nouvelle machine"},
)
```

`update_client_metadata` validates locally that `project_id` is an `int` and `metadata` is non-empty before calling `POST /api/client_metadata`.

## Cryptographic signing (Ed25519)

Each project has an Ed25519 key pair. The server signs every validation response; the SDK encrypts the HWID with SealedBox and verifies signatures automatically.

### Enable / disable crypto (owner only)

```python
manager.toggle_crypto(project_id=42, enabled=True)
manager.toggle_crypto(project_id=42, enabled=False)
```

### Retrieve the public key

```python
# Via ManagerClient (authenticated)
data = manager.get_public_key(project_id=42)
public_key_hex = data["public_key"]

# Via LicenseClient (public endpoint, no auth)
client.fetch_public_key()          # fetches & caches on client.public_key
print(client.public_key)
```

### Rotate the signing key (owner only)

```python
result = manager.rotate_signing_key(project_id=42)
print(result["signing_public_key"])  # distribute this new key to all SDK clients
```

> **WARNING** — rotating invalidates all SDK instances still holding the old public key.

### Manual signature verification

```python
from authlix import verify_signature, SignatureVerificationError

try:
    verify_signature(public_key_hex, response_payload)
except SignatureVerificationError as exc:
    print(f"Signature invalid: {exc}")
```

## Environment helpers

```python
from authlix import collect_environment_metadata, get_machine_fingerprint, get_public_ip

# Stable machine fingerprint (SHA-256 of MAC + platform info)
hwid = get_machine_fingerprint()

# Public IP via ipify
ip = get_public_ip()

# Both at once (public IP optional)
meta = collect_environment_metadata(include_public_ip=True)
print(meta)  # {"hwid": "...", "ip": "..."}
```

## Webhooks

Subscribe to license lifecycle events (created, activated, expired, revoked, quota exceeded) and receive real-time HTTP callbacks.

```python
from authlix import ManagerClient

manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("admin", api_key="sk_live_XXXXXXXXXXXXXXXX")

# Register a webhook endpoint
webhook = manager.create_webhook(
    project_id=42,
    url="https://my-app.io/hooks/authlix",
    events=["license.created", "license.revoked", "quota.exceeded"],
    secret="whsec_my_signing_secret",  # optional HMAC secret
)
print(f"Webhook #{webhook['id']} registered")

# List endpoints for a project
webhooks = manager.list_webhooks(project_id=42)

# Update events or URL
manager.update_webhook(webhook_id=webhook["id"], events=["license.created"])

# Send a test ping
manager.test_webhook(webhook_id=webhook["id"])

# View delivery history
deliveries = manager.list_webhook_deliveries(webhook_id=webhook["id"])

# Remove when no longer needed
manager.delete_webhook(webhook_id=webhook["id"])
```

Payloads are signed with HMAC-SHA256 when a `secret` is provided. Verify the `X-Authlix-Signature` header on your server.

## Audit logs

Every significant action (license CRUD, API key changes, webhook updates) is recorded in an append-only audit log.

```python
# List recent events for a project
logs = manager.list_audit_logs(project_id=42, page=1, per_page=50)
for entry in logs["items"]:
    print(f"{entry['timestamp']} {entry['action']} by {entry['actor']}")

# Export full audit trail as CSV or JSON
csv_data = manager.export_audit_logs(project_id=42, fmt="csv")
with open("audit.csv", "w") as f:
    f.write(csv_data)

json_data = manager.export_audit_logs(project_id=42, fmt="json")
```

## Quotas & usage limits

Projects can define quota policies (max activations, max devices, grace periods). The SDK exposes usage summaries.

```python
from authlix import QuotaExceededError

# View usage summary for a license
usage = manager.get_usage_summary(license_id=123)
print(f"Activations: {usage['activation_count']}/{usage['max_activations']}")
print(f"Devices: {usage['device_count']}/{usage['max_devices']}")

# Quota enforcement happens automatically during validation
from authlix import LicenseClient

client = LicenseClient(base_url="https://authlix.io", project_id=42)
try:
    result = client.validate_with_environment("CL-XXXX-XXXX")
    quota = result.get("quota_info", {})
    print(f"Quota status: {quota.get('quota_status')}")
except QuotaExceededError as exc:
    print(f"Quota exceeded: {exc}")
```

## Offline activation

Issue time-limited Ed25519-signed tokens for environments without Internet access. Tokens can be verified locally without contacting the server.

```python
from authlix import ManagerClient, LicenseClient

manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("admin", api_key="sk_live_XXXXXXXXXXXXXXXX")

# Issue an offline token (server-side)
result = manager.issue_offline_token(
    license_id=123,
    machine_hash="sha256-of-machine-fingerprint",
    ttl_days=90,
)
offline_token = result["token"]
print(f"Token valid until: {result['expires_at']}")

# Verify locally on the client machine (no network needed)
payload = LicenseClient.verify_offline_token_locally(
    offline_token,
    public_key_hex="<project-ed25519-public-key>",
)
print(f"License #{payload['license_id']} verified offline until {payload['exp']}")

# Revoke a compromised token (server-side)
manager.revoke_offline_token(token_id=result["id"])

# List all offline tokens for a license
tokens = manager.list_offline_tokens(license_id=123)
```

## Error handling

```python
from authlix import (
    AuthlixError,
    AuthenticationError,
    ApiError,
    BadRequestError,
    ForbiddenError,
    NotFoundError,
    QuotaExceededError,
    OfflineTokenError,
    SignatureVerificationError,
)

try:
    result = client.validate_with_environment("CL-XXXX-XXXX")
except SignatureVerificationError as exc:
    # Ed25519 signature mismatch — possible tampering
    print(f"Signature invalid: {exc}")
except QuotaExceededError as exc:
    # License exceeded activation/device limits
    print(f"Quota exceeded (429): {exc}")
except BadRequestError as exc:
    print(f"Bad request (400): {exc}")
except ForbiddenError as exc:
    print(f"HWID mismatch or inactive license (403): {exc}")
except NotFoundError as exc:
    print(f"License not found (404): {exc}")
except ApiError as exc:
    print(f"API error {exc.status_code}: {exc}  payload={exc.payload}")
except AuthlixError as exc:
    print(f"SDK error: {exc}")
```

| Exception | HTTP | When |
|---|---|---|
| `BadRequestError` | 400 | Missing / invalid parameters |
| `ForbiddenError` | 403 | HWID mismatch, inactive license, insufficient scope |
| `NotFoundError` | 404 | Unknown license key or project |
| `QuotaExceededError` | 429 | Activation or device limit exceeded |
| `OfflineTokenError` | — | Offline token expired, revoked, or signature invalid |
| `SignatureVerificationError` | — | Ed25519 signature verification failed |
| `ApiError` | ≥400 | Any other server error |
| `AuthenticationError` | — | Login failed or JWT missing |
| `AuthlixError` | — | Base class for all SDK exceptions |

## ManagerClient reference

| Method | Description |
|---|---|
| **Auth** | |
| `authenticate(username, *, password, api_key, hcaptcha_token)` | Login and store JWT |
| **Projects** | |
| `list_projects()` | List all accessible projects |
| `create_project(name, description)` | Create a new project |
| `add_project_manager(project_id, username)` | Delegate back-office access |
| `update_metadata_schema(project_id, fields)` | Define project metadata schema |
| **Licenses** | |
| `list_licenses(project_id)` | List all licenses for a project |
| `get_license_by_key(project_id, license_key)` | Efficient single-key lookup |
| `create_license(project_id, days_valid, metadata)` | Generate a new license |
| `update_license(license_id, *, is_active, expires_at, reset_hwid, metadata)` | Update a license |
| `extend_license(project_id, license_id, days)` | Push expiration forward |
| `delete_license(license_id)` | Hard-delete a license |
| **API Keys** | |
| `list_api_keys()` | Operator-wide API key list |
| `create_api_key(label, *, project_ids, allow_all_projects, scopes)` | Mint a scoped API key |
| `update_api_key(api_key_id, *, label, project_ids, allow_all_projects)` | Update key scope |
| `delete_api_key(api_key_id)` | Revoke an API key |
| `list_project_api_keys(project_id)` | Per-project API key list |
| **Crypto** | |
| `get_public_key(project_id)` | Fetch Ed25519 public key (no auth) |
| `toggle_crypto(project_id, enabled)` | Enable/disable signing (owner) |
| `rotate_signing_key(project_id)` | Regenerate key pair (owner) |
| **Webhooks** | |
| `list_webhooks(project_id)` | List webhook endpoints |
| `create_webhook(project_id, url, *, events, secret)` | Register a webhook |
| `update_webhook(webhook_id, *, url, events, is_active)` | Update a webhook |
| `delete_webhook(webhook_id)` | Remove a webhook |
| `test_webhook(webhook_id)` | Send a test ping |
| `list_webhook_deliveries(webhook_id)` | View delivery history |
| **Audit** | |
| `list_audit_logs(project_id, *, page, per_page)` | Browse audit events |
| `export_audit_logs(project_id, *, fmt)` | Export as `csv` or `json` |
| **Quotas** | |
| `get_usage_summary(license_id)` | Activation/device usage stats |
| **Offline Activation** | |
| `issue_offline_token(license_id, machine_hash, *, ttl_days)` | Generate offline token |
| `revoke_offline_token(token_id)` | Revoke a token |
| `list_offline_tokens(license_id)` | List tokens for a license |

## LicenseClient reference

| Method | Description |
|---|---|
| `validate_license(key, *, hwid, ip, metadata)` | Validate a license key |
| `validate_with_environment(key)` | Validate with auto-collected HWID/IP |
| `update_client_metadata(key, metadata)` | Update client-editable fields |
| `fetch_public_key()` | Fetch and cache Ed25519 public key |
| `verify_offline_token_locally(token, public_key_hex)` | Verify offline token without network (static) |

## Examples

See the [`examples/`](examples/) directory for runnable scripts:

- [`examples/client/`](examples/client/) — validation, crypto, metadata update, offline tokens, error handling
- [`examples/manager/`](examples/manager/) — project setup, license management, API keys, crypto, webhooks, audit logs, quotas, offline activation, scopes

## Local development

```bash
python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -e .
python -m pytest tests/ -v
python -m build
```

