Metadata-Version: 2.4
Name: authlix
Version: 0.4.0
Summary: Client and manager helpers for Authlix
Author-email: Authlix <me@showdown.boo>
Project-URL: Homepage, https://authlix.io/
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.31.0
Requires-Dist: PyNaCl>=1.5.0

# Authlix Python SDK

> 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

```python
manager = ManagerClient(base_url="https://authlix.io")
manager.authenticate("admin", password="password")

# Create a key scoped to one project (raw secret returned once)
api_key = manager.create_api_key(
    label="CI Runner",
    project_ids=[project["id"]],
)

# 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"])
```

## 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=[
        {"name": "notes", "type": "string", "client_editable": True},
        {"name": "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": "..."}
```

## Error handling

```python
from authlix import (
    AuthlixError,
    AuthenticationError,
    ApiError,
    BadRequestError,
    ForbiddenError,
    NotFoundError,
    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 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 |
| `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 |
|---|---|
| `authenticate(username, *, password, api_key)` | Login and store JWT |
| `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 |
| `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 |
| `list_api_keys()` | Operator-wide API key list |
| `create_api_key(label, *, project_ids, allow_all_projects)` | 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 |
| `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) |

## Examples

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

- [`examples/client/`](examples/client/) — validation, crypto, metadata update, error handling
- [`examples/manager/`](examples/manager/) — project setup, license management, API keys, crypto

## 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
```

