authlix 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- authlix-0.3.0/PKG-INFO +240 -0
- authlix-0.3.0/README.md +230 -0
- authlix-0.3.0/authlix/__init__.py +39 -0
- authlix-0.3.0/authlix/client.py +142 -0
- authlix-0.3.0/authlix/environment.py +53 -0
- authlix-0.3.0/authlix/exceptions.py +39 -0
- authlix-0.3.0/authlix/manager.py +248 -0
- authlix-0.3.0/authlix.egg-info/PKG-INFO +240 -0
- authlix-0.3.0/authlix.egg-info/SOURCES.txt +12 -0
- authlix-0.3.0/authlix.egg-info/dependency_links.txt +1 -0
- authlix-0.3.0/authlix.egg-info/requires.txt +1 -0
- authlix-0.3.0/authlix.egg-info/top_level.txt +1 -0
- authlix-0.3.0/pyproject.toml +19 -0
- authlix-0.3.0/setup.cfg +4 -0
authlix-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authlix
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Client and manager helpers for Authlix
|
|
5
|
+
Author-email: Authlix <me@showdown.boo>
|
|
6
|
+
Project-URL: Homepage, https://authlix.io/
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
|
|
11
|
+
# Authlix Python SDK
|
|
12
|
+
|
|
13
|
+
> Client and manager helpers for integrating public validation flows and project operations with the Authlix API.
|
|
14
|
+
|
|
15
|
+
**License client** – validates license keys, collects environment metadata and lets end-users update `client_editable` fields.
|
|
16
|
+
**Manager client** – handles authentication plus CRUD operations for projects, metadata schemas and licenses.
|
|
17
|
+
**Environment helpers** – gather HWID, local/public IPs and other contextual information in one call.
|
|
18
|
+
|
|
19
|
+
The SDK targets Python 3.9+ and is published to PyPI as `authlix`.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install authlix
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
### Validate a license key
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from authlix import LicenseClient
|
|
35
|
+
|
|
36
|
+
client = LicenseClient(
|
|
37
|
+
base_url="https://authlix.io",
|
|
38
|
+
project_id=42,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Automatically collects HWID, LAN/public IP, etc.
|
|
42
|
+
result = client.validate_with_environment("CL-XXXX-XXXX")
|
|
43
|
+
print(result)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
### Manage projects and licenses
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from authlix import ManagerClient
|
|
52
|
+
|
|
53
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
54
|
+
manager.authenticate("automation_bot", api_key="sk_live_XXXXXXXXXXXXXXXX")
|
|
55
|
+
|
|
56
|
+
# Create project and create license
|
|
57
|
+
project = manager.create_project(
|
|
58
|
+
name="My Awesome App",
|
|
59
|
+
description="Tooling pour la communauté",
|
|
60
|
+
)
|
|
61
|
+
license_data = manager.create_license(
|
|
62
|
+
project_id=project["id"],
|
|
63
|
+
days_valid=30,
|
|
64
|
+
metadata={"plan": "pro"},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Revoke a license
|
|
68
|
+
manager.update_license(
|
|
69
|
+
license_id=license_data["id"],
|
|
70
|
+
is_active=False,
|
|
71
|
+
metadata={"plan": "revoked", "notes": "Chargeback"},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Update a license
|
|
75
|
+
manager.update_license(
|
|
76
|
+
license_id=license_data["id"],
|
|
77
|
+
metadata={"plan": "enterprise", "notes": "Upgraded"},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Delegate day-to-day operations to another teammate
|
|
81
|
+
manager.add_project_manager(project_id=project["id"], username="support_team")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
More helpers are exposed for listing projects/licenses, extending expirations, resetting HWIDs, or deleting keys entirely.
|
|
85
|
+
|
|
86
|
+
### Look up a license by key
|
|
87
|
+
|
|
88
|
+
For efficiency, use `get_license_by_key` when you only need to look up a single license instead of fetching all licenses:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
try:
|
|
92
|
+
license_data = manager.get_license_by_key(
|
|
93
|
+
project_id=42,
|
|
94
|
+
license_key="CL-XXXX-XXXX"
|
|
95
|
+
)
|
|
96
|
+
print(f"License ID: {license_data['id']}, Active: {license_data['is_active']}")
|
|
97
|
+
except NotFoundError:
|
|
98
|
+
print("License key not found in this project")
|
|
99
|
+
except ForbiddenError:
|
|
100
|
+
print("Insufficient permissions for this project")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The key is automatically normalized to uppercase to avoid case mismatches. This method is ideal for dashboards and support tools that need to quickly retrieve license details by key.
|
|
104
|
+
|
|
105
|
+
### API key management
|
|
106
|
+
|
|
107
|
+
Automations or SDKs can provision scoped API keys and rotate them without touching end-user credentials.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
111
|
+
manager.authenticate("admin", password="password")
|
|
112
|
+
|
|
113
|
+
# Create a key limited to a project (secret is returned once)
|
|
114
|
+
api_key = manager.create_api_key(
|
|
115
|
+
label="CI Runner",
|
|
116
|
+
project_ids=[project["id"]],
|
|
117
|
+
allow_all_projects=False,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Update its scopes or friendly label
|
|
121
|
+
manager.update_api_key(
|
|
122
|
+
api_key_id=api_key["id"],
|
|
123
|
+
project_ids=[project["id"], 7],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Dashboard helper: only show keys relevant to a project
|
|
127
|
+
manager.list_project_api_keys(project_id=project["id"])
|
|
128
|
+
|
|
129
|
+
# Cleanup when the automation is retired
|
|
130
|
+
manager.delete_api_key(api_key_id=api_key["id"])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use `manager.list_api_keys()` to render an operator-wide view (active + revoked keys).
|
|
134
|
+
|
|
135
|
+
## Client-editable metadata workflow
|
|
136
|
+
|
|
137
|
+
Certain metadata fields can be safely mutated by the end-user thanks to the `client_editable` flag defined in the project schema.
|
|
138
|
+
|
|
139
|
+
1. **Manager** defines the schema:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from authlix import ManagerClient
|
|
143
|
+
|
|
144
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
145
|
+
manager.authenticate("admin", "password")
|
|
146
|
+
|
|
147
|
+
manager.update_metadata_schema(
|
|
148
|
+
project_id=42,
|
|
149
|
+
fields=[
|
|
150
|
+
{"name": "notes", "type": "string", "client_editable": True},
|
|
151
|
+
{"name": "plan", "type": "string", "client_editable": False},
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. **Client** updates the editable fields:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from authlix import LicenseClient
|
|
160
|
+
|
|
161
|
+
client = LicenseClient(base_url="https://authlix.io", project_id=42)
|
|
162
|
+
|
|
163
|
+
client.update_client_metadata(
|
|
164
|
+
key="CL-XXXX-XXXX",
|
|
165
|
+
metadata={"notes": "Nouvelle machine"},
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`update_client_metadata` validates locally that `project_id` is an `int` and the `metadata` dictionary is non-empty before calling `POST /api/client_metadata`. Server responses bubble up any JSON `msg` errors through `ApiError` for easier debugging (e.g., non editable field, inactive key, etc.).
|
|
170
|
+
|
|
171
|
+
## Environment helpers
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from authlix import collect_environment_metadata
|
|
175
|
+
|
|
176
|
+
print(collect_environment_metadata())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The helper returns HWID, hostname, LAN IP, optional public IP, and other useful attributes. Use it to enrich `validate_license` calls or to store telemetry on validated machines.
|
|
180
|
+
|
|
181
|
+
## Error handling
|
|
182
|
+
|
|
183
|
+
The SDK provides specific exception classes for common HTTP errors:
|
|
184
|
+
|
|
185
|
+
- `BadRequestError` – raised for HTTP 400 (Bad Request, e.g., missing or invalid parameters)
|
|
186
|
+
- `ForbiddenError` – raised for HTTP 403 (Forbidden, e.g., insufficient project scope)
|
|
187
|
+
- `NotFoundError` – raised for HTTP 404 (Not Found, e.g., unknown license key)
|
|
188
|
+
- `ApiError(status_code, message, payload)` – generic error for other HTTP ≥ 400 responses
|
|
189
|
+
- `AuthenticationError` – surfaces authentication/authorization issues in manager flows
|
|
190
|
+
- `AuthlixError` – base class for all SDK exceptions for easy broad exception handling
|
|
191
|
+
|
|
192
|
+
All error classes include a `status_code` and `payload` for detailed debugging:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from authlix import ApiError, BadRequestError, ForbiddenError, NotFoundError
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
license_data = manager.get_license_by_key(project_id=42, license_key="CL-XXXX")
|
|
199
|
+
except BadRequestError as exc:
|
|
200
|
+
print(f"Invalid request: {exc}")
|
|
201
|
+
except ForbiddenError as exc:
|
|
202
|
+
print(f"Access denied to project: {exc}")
|
|
203
|
+
except NotFoundError as exc:
|
|
204
|
+
print(f"License not found: {exc}")
|
|
205
|
+
except ApiError as exc:
|
|
206
|
+
print(f"API error {exc.status_code}: {exc}")
|
|
207
|
+
print(exc.payload)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Local development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
python -m venv .venv
|
|
214
|
+
.venv\Scripts\activate
|
|
215
|
+
pip install -e .
|
|
216
|
+
python -m build
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Additional helpers:
|
|
220
|
+
|
|
221
|
+
- `list_licenses(project_id)` – list all licenses (keys + live metadata and usage counters) for a project (prefer `get_license_by_key` for single lookups)
|
|
222
|
+
- `get_license_by_key(project_id, license_key)` – efficiently retrieve a single license by its key (automatically normalized to uppercase)
|
|
223
|
+
- `create_license(project_id, days_valid=None, metadata=None)` – generate a new license key
|
|
224
|
+
- `update_license(license_id, is_active=None, expires_at=None, reset_hwid=False, metadata=None)` – ban/disable, move expiration, reset HWID, or overwrite metadata
|
|
225
|
+
- `extend_license(project_id, license_id, days)` – convenience method to push expiration forward
|
|
226
|
+
- `delete_license(license_id)` – hard-delete a key
|
|
227
|
+
- `add_project_manager(project_id, username)` – delegate back-office access to teammates
|
|
228
|
+
- `create_api_key(label, project_ids=None, allow_all_projects=False)` – mint scoped API keys from the dashboard
|
|
229
|
+
- `update_api_key(api_key_id, ...)`/`delete_api_key(api_key_id)` – rotate or revoke keys instantly
|
|
230
|
+
- `list_api_keys()`/`list_project_api_keys(project_id)` – audit keys globally or per project
|
|
231
|
+
|
|
232
|
+
## Environment helpers
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from authlix import collect_environment_metadata
|
|
236
|
+
|
|
237
|
+
print(collect_environment_metadata())
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Returns HWID, hostname, LAN IP, and (if available) public IP.
|
authlix-0.3.0/README.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# Authlix Python SDK
|
|
2
|
+
|
|
3
|
+
> Client and manager helpers for integrating public validation flows and project operations with the Authlix API.
|
|
4
|
+
|
|
5
|
+
**License client** – validates license keys, collects environment metadata and lets end-users update `client_editable` fields.
|
|
6
|
+
**Manager client** – handles authentication plus CRUD operations for projects, metadata schemas and licenses.
|
|
7
|
+
**Environment helpers** – gather HWID, local/public IPs and other contextual information in one call.
|
|
8
|
+
|
|
9
|
+
The SDK targets Python 3.9+ and is published to PyPI as `authlix`.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install authlix
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
### Validate a license key
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from authlix import LicenseClient
|
|
25
|
+
|
|
26
|
+
client = LicenseClient(
|
|
27
|
+
base_url="https://authlix.io",
|
|
28
|
+
project_id=42,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Automatically collects HWID, LAN/public IP, etc.
|
|
32
|
+
result = client.validate_with_environment("CL-XXXX-XXXX")
|
|
33
|
+
print(result)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
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.
|
|
37
|
+
|
|
38
|
+
### Manage projects and licenses
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from authlix import ManagerClient
|
|
42
|
+
|
|
43
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
44
|
+
manager.authenticate("automation_bot", api_key="sk_live_XXXXXXXXXXXXXXXX")
|
|
45
|
+
|
|
46
|
+
# Create project and create license
|
|
47
|
+
project = manager.create_project(
|
|
48
|
+
name="My Awesome App",
|
|
49
|
+
description="Tooling pour la communauté",
|
|
50
|
+
)
|
|
51
|
+
license_data = manager.create_license(
|
|
52
|
+
project_id=project["id"],
|
|
53
|
+
days_valid=30,
|
|
54
|
+
metadata={"plan": "pro"},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Revoke a license
|
|
58
|
+
manager.update_license(
|
|
59
|
+
license_id=license_data["id"],
|
|
60
|
+
is_active=False,
|
|
61
|
+
metadata={"plan": "revoked", "notes": "Chargeback"},
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Update a license
|
|
65
|
+
manager.update_license(
|
|
66
|
+
license_id=license_data["id"],
|
|
67
|
+
metadata={"plan": "enterprise", "notes": "Upgraded"},
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Delegate day-to-day operations to another teammate
|
|
71
|
+
manager.add_project_manager(project_id=project["id"], username="support_team")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
More helpers are exposed for listing projects/licenses, extending expirations, resetting HWIDs, or deleting keys entirely.
|
|
75
|
+
|
|
76
|
+
### Look up a license by key
|
|
77
|
+
|
|
78
|
+
For efficiency, use `get_license_by_key` when you only need to look up a single license instead of fetching all licenses:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
try:
|
|
82
|
+
license_data = manager.get_license_by_key(
|
|
83
|
+
project_id=42,
|
|
84
|
+
license_key="CL-XXXX-XXXX"
|
|
85
|
+
)
|
|
86
|
+
print(f"License ID: {license_data['id']}, Active: {license_data['is_active']}")
|
|
87
|
+
except NotFoundError:
|
|
88
|
+
print("License key not found in this project")
|
|
89
|
+
except ForbiddenError:
|
|
90
|
+
print("Insufficient permissions for this project")
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The key is automatically normalized to uppercase to avoid case mismatches. This method is ideal for dashboards and support tools that need to quickly retrieve license details by key.
|
|
94
|
+
|
|
95
|
+
### API key management
|
|
96
|
+
|
|
97
|
+
Automations or SDKs can provision scoped API keys and rotate them without touching end-user credentials.
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
101
|
+
manager.authenticate("admin", password="password")
|
|
102
|
+
|
|
103
|
+
# Create a key limited to a project (secret is returned once)
|
|
104
|
+
api_key = manager.create_api_key(
|
|
105
|
+
label="CI Runner",
|
|
106
|
+
project_ids=[project["id"]],
|
|
107
|
+
allow_all_projects=False,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Update its scopes or friendly label
|
|
111
|
+
manager.update_api_key(
|
|
112
|
+
api_key_id=api_key["id"],
|
|
113
|
+
project_ids=[project["id"], 7],
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Dashboard helper: only show keys relevant to a project
|
|
117
|
+
manager.list_project_api_keys(project_id=project["id"])
|
|
118
|
+
|
|
119
|
+
# Cleanup when the automation is retired
|
|
120
|
+
manager.delete_api_key(api_key_id=api_key["id"])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Use `manager.list_api_keys()` to render an operator-wide view (active + revoked keys).
|
|
124
|
+
|
|
125
|
+
## Client-editable metadata workflow
|
|
126
|
+
|
|
127
|
+
Certain metadata fields can be safely mutated by the end-user thanks to the `client_editable` flag defined in the project schema.
|
|
128
|
+
|
|
129
|
+
1. **Manager** defines the schema:
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from authlix import ManagerClient
|
|
133
|
+
|
|
134
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
135
|
+
manager.authenticate("admin", "password")
|
|
136
|
+
|
|
137
|
+
manager.update_metadata_schema(
|
|
138
|
+
project_id=42,
|
|
139
|
+
fields=[
|
|
140
|
+
{"name": "notes", "type": "string", "client_editable": True},
|
|
141
|
+
{"name": "plan", "type": "string", "client_editable": False},
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
2. **Client** updates the editable fields:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from authlix import LicenseClient
|
|
150
|
+
|
|
151
|
+
client = LicenseClient(base_url="https://authlix.io", project_id=42)
|
|
152
|
+
|
|
153
|
+
client.update_client_metadata(
|
|
154
|
+
key="CL-XXXX-XXXX",
|
|
155
|
+
metadata={"notes": "Nouvelle machine"},
|
|
156
|
+
)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`update_client_metadata` validates locally that `project_id` is an `int` and the `metadata` dictionary is non-empty before calling `POST /api/client_metadata`. Server responses bubble up any JSON `msg` errors through `ApiError` for easier debugging (e.g., non editable field, inactive key, etc.).
|
|
160
|
+
|
|
161
|
+
## Environment helpers
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from authlix import collect_environment_metadata
|
|
165
|
+
|
|
166
|
+
print(collect_environment_metadata())
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The helper returns HWID, hostname, LAN IP, optional public IP, and other useful attributes. Use it to enrich `validate_license` calls or to store telemetry on validated machines.
|
|
170
|
+
|
|
171
|
+
## Error handling
|
|
172
|
+
|
|
173
|
+
The SDK provides specific exception classes for common HTTP errors:
|
|
174
|
+
|
|
175
|
+
- `BadRequestError` – raised for HTTP 400 (Bad Request, e.g., missing or invalid parameters)
|
|
176
|
+
- `ForbiddenError` – raised for HTTP 403 (Forbidden, e.g., insufficient project scope)
|
|
177
|
+
- `NotFoundError` – raised for HTTP 404 (Not Found, e.g., unknown license key)
|
|
178
|
+
- `ApiError(status_code, message, payload)` – generic error for other HTTP ≥ 400 responses
|
|
179
|
+
- `AuthenticationError` – surfaces authentication/authorization issues in manager flows
|
|
180
|
+
- `AuthlixError` – base class for all SDK exceptions for easy broad exception handling
|
|
181
|
+
|
|
182
|
+
All error classes include a `status_code` and `payload` for detailed debugging:
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
from authlix import ApiError, BadRequestError, ForbiddenError, NotFoundError
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
license_data = manager.get_license_by_key(project_id=42, license_key="CL-XXXX")
|
|
189
|
+
except BadRequestError as exc:
|
|
190
|
+
print(f"Invalid request: {exc}")
|
|
191
|
+
except ForbiddenError as exc:
|
|
192
|
+
print(f"Access denied to project: {exc}")
|
|
193
|
+
except NotFoundError as exc:
|
|
194
|
+
print(f"License not found: {exc}")
|
|
195
|
+
except ApiError as exc:
|
|
196
|
+
print(f"API error {exc.status_code}: {exc}")
|
|
197
|
+
print(exc.payload)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Local development
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
python -m venv .venv
|
|
204
|
+
.venv\Scripts\activate
|
|
205
|
+
pip install -e .
|
|
206
|
+
python -m build
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Additional helpers:
|
|
210
|
+
|
|
211
|
+
- `list_licenses(project_id)` – list all licenses (keys + live metadata and usage counters) for a project (prefer `get_license_by_key` for single lookups)
|
|
212
|
+
- `get_license_by_key(project_id, license_key)` – efficiently retrieve a single license by its key (automatically normalized to uppercase)
|
|
213
|
+
- `create_license(project_id, days_valid=None, metadata=None)` – generate a new license key
|
|
214
|
+
- `update_license(license_id, is_active=None, expires_at=None, reset_hwid=False, metadata=None)` – ban/disable, move expiration, reset HWID, or overwrite metadata
|
|
215
|
+
- `extend_license(project_id, license_id, days)` – convenience method to push expiration forward
|
|
216
|
+
- `delete_license(license_id)` – hard-delete a key
|
|
217
|
+
- `add_project_manager(project_id, username)` – delegate back-office access to teammates
|
|
218
|
+
- `create_api_key(label, project_ids=None, allow_all_projects=False)` – mint scoped API keys from the dashboard
|
|
219
|
+
- `update_api_key(api_key_id, ...)`/`delete_api_key(api_key_id)` – rotate or revoke keys instantly
|
|
220
|
+
- `list_api_keys()`/`list_project_api_keys(project_id)` – audit keys globally or per project
|
|
221
|
+
|
|
222
|
+
## Environment helpers
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from authlix import collect_environment_metadata
|
|
226
|
+
|
|
227
|
+
print(collect_environment_metadata())
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Returns HWID, hostname, LAN IP, and (if available) public IP.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Official Authlix helper library.
|
|
2
|
+
|
|
3
|
+
This package exposes two primary interfaces:
|
|
4
|
+
|
|
5
|
+
- :class:`authlix.client.LicenseClient` for end-users validating licenses
|
|
6
|
+
- :class:`authlix.manager.ManagerClient` for operators managing projects
|
|
7
|
+
|
|
8
|
+
Utility helpers live in :mod:`authlix.environment` for gathering HWID/IP metadata.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .client import LicenseClient
|
|
12
|
+
from .manager import ManagerClient
|
|
13
|
+
from .environment import (
|
|
14
|
+
get_machine_fingerprint,
|
|
15
|
+
get_public_ip,
|
|
16
|
+
collect_environment_metadata,
|
|
17
|
+
)
|
|
18
|
+
from .exceptions import (
|
|
19
|
+
AuthlixError,
|
|
20
|
+
AuthenticationError,
|
|
21
|
+
ApiError,
|
|
22
|
+
BadRequestError,
|
|
23
|
+
ForbiddenError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"LicenseClient",
|
|
29
|
+
"ManagerClient",
|
|
30
|
+
"get_machine_fingerprint",
|
|
31
|
+
"get_public_ip",
|
|
32
|
+
"collect_environment_metadata",
|
|
33
|
+
"AuthlixError",
|
|
34
|
+
"AuthenticationError",
|
|
35
|
+
"ApiError",
|
|
36
|
+
"BadRequestError",
|
|
37
|
+
"ForbiddenError",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Client-facing helpers for validating Authlix keys."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, Optional
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
from .environment import collect_environment_metadata, get_machine_fingerprint, get_public_ip
|
|
10
|
+
from .exceptions import ApiError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LicenseClient:
|
|
14
|
+
"""Simple wrapper for the public validation endpoint.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
base_url:
|
|
19
|
+
Base URL of the CyberLicensing API (e.g. ``"https://licensing.example.com"``).
|
|
20
|
+
project_id:
|
|
21
|
+
Owning project id, required by ``POST /api/validate_license`` to scope
|
|
22
|
+
licenses per product.
|
|
23
|
+
timeout:
|
|
24
|
+
Optional HTTP timeout in seconds.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, base_url: str, project_id: int, timeout: float = 5.0):
|
|
28
|
+
self.base_url = base_url.rstrip('/')
|
|
29
|
+
self.project_id = project_id
|
|
30
|
+
self.timeout = timeout
|
|
31
|
+
|
|
32
|
+
def _handle_response(self, response: requests.Response) -> Dict:
|
|
33
|
+
if response.status_code >= 400:
|
|
34
|
+
payload: Optional[Dict[str, Any]] = None
|
|
35
|
+
if response.content:
|
|
36
|
+
try:
|
|
37
|
+
payload = response.json()
|
|
38
|
+
except ValueError:
|
|
39
|
+
payload = None
|
|
40
|
+
message = (
|
|
41
|
+
payload.get("msg")
|
|
42
|
+
if isinstance(payload, dict) and payload.get("msg")
|
|
43
|
+
else response.text
|
|
44
|
+
)
|
|
45
|
+
raise ApiError(response.status_code, message, payload)
|
|
46
|
+
if response.content:
|
|
47
|
+
try:
|
|
48
|
+
return response.json()
|
|
49
|
+
except ValueError as exc:
|
|
50
|
+
raise ApiError(response.status_code, f"Invalid JSON response: {exc}", None)
|
|
51
|
+
return {}
|
|
52
|
+
|
|
53
|
+
def validate_license(
|
|
54
|
+
self,
|
|
55
|
+
key: str,
|
|
56
|
+
hwid: Optional[str] = None,
|
|
57
|
+
ip: Optional[str] = None,
|
|
58
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
59
|
+
) -> Dict:
|
|
60
|
+
"""Validate a license key against the public endpoint.
|
|
61
|
+
|
|
62
|
+
The request payload matches the backend contract::
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
"project_id": 1,
|
|
66
|
+
"key": "XXXX-XXXX-XXXX-XXXX",
|
|
67
|
+
"hwid": "unique-hardware-id"
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
``metadata`` can be supplied to share additional key-value pairs, but it
|
|
71
|
+
must only contain fields flagged as ``client_editable`` in the project
|
|
72
|
+
metadata schema configured by the project manager.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
payload: Dict[str, Any] = {
|
|
76
|
+
"project_id": self.project_id,
|
|
77
|
+
"key": key,
|
|
78
|
+
}
|
|
79
|
+
if hwid:
|
|
80
|
+
payload["hwid"] = hwid
|
|
81
|
+
if ip:
|
|
82
|
+
payload["ip"] = ip
|
|
83
|
+
if metadata:
|
|
84
|
+
payload["metadata"] = metadata
|
|
85
|
+
|
|
86
|
+
response = requests.post(
|
|
87
|
+
f"{self.base_url}/api/validate_license",
|
|
88
|
+
json=payload,
|
|
89
|
+
timeout=self.timeout,
|
|
90
|
+
)
|
|
91
|
+
return self._handle_response(response)
|
|
92
|
+
|
|
93
|
+
def validate_with_environment(self, key: str, extra_metadata: Optional[Dict[str, Any]] = None) -> Dict:
|
|
94
|
+
"""Validate a key while auto-populating the machine fingerprint as HWID.
|
|
95
|
+
|
|
96
|
+
The public IP is captured automatically server-side, no need to send it.
|
|
97
|
+
Only extra_metadata with client_editable fields can be provided.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
hwid = get_machine_fingerprint()
|
|
101
|
+
|
|
102
|
+
return self.validate_license(
|
|
103
|
+
key=key,
|
|
104
|
+
hwid=hwid,
|
|
105
|
+
metadata=extra_metadata if extra_metadata else None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def update_client_metadata(self, key: str, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
109
|
+
"""Update client-editable metadata fields for a specific key.
|
|
110
|
+
|
|
111
|
+
Parameters
|
|
112
|
+
----------
|
|
113
|
+
key:
|
|
114
|
+
License key to mutate.
|
|
115
|
+
metadata:
|
|
116
|
+
Non-empty dictionary of ``client_editable`` fields to update.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
if not isinstance(self.project_id, int):
|
|
120
|
+
raise ValueError("project_id must be an integer")
|
|
121
|
+
if not isinstance(metadata, dict) or not metadata:
|
|
122
|
+
raise ValueError("metadata must be a non-empty dictionary")
|
|
123
|
+
|
|
124
|
+
payload = {
|
|
125
|
+
"project_id": self.project_id,
|
|
126
|
+
"key": key,
|
|
127
|
+
"metadata": metadata,
|
|
128
|
+
}
|
|
129
|
+
response = requests.post(
|
|
130
|
+
f"{self.base_url}/api/client_metadata",
|
|
131
|
+
json=payload,
|
|
132
|
+
timeout=self.timeout,
|
|
133
|
+
)
|
|
134
|
+
return self._handle_response(response)
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def get_default_hwid() -> str:
|
|
138
|
+
return get_machine_fingerprint()
|
|
139
|
+
|
|
140
|
+
@staticmethod
|
|
141
|
+
def get_default_ip() -> Optional[str]:
|
|
142
|
+
return get_public_ip()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Environment helpers for Authlix SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import platform
|
|
7
|
+
import socket
|
|
8
|
+
import uuid
|
|
9
|
+
from typing import Dict, Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
PUBLIC_IP_ENDPOINT = "https://api.ipify.org?format=json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_public_ip(timeout: float = 3.0) -> Optional[str]:
|
|
17
|
+
"""Return the machine's public IP using a lightweight HTTPS call."""
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
response = requests.get(PUBLIC_IP_ENDPOINT, timeout=timeout)
|
|
21
|
+
response.raise_for_status()
|
|
22
|
+
return response.json().get("ip")
|
|
23
|
+
except Exception:
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_machine_fingerprint() -> str:
|
|
28
|
+
"""Compute a stable machine fingerprint using MAC + platform info."""
|
|
29
|
+
|
|
30
|
+
mac = uuid.getnode()
|
|
31
|
+
platform_bits = platform.platform()
|
|
32
|
+
processor = platform.processor()
|
|
33
|
+
base_string = f"{mac}-{platform_bits}-{processor}"
|
|
34
|
+
digest = hashlib.sha256(base_string.encode("utf-8")).hexdigest()
|
|
35
|
+
return digest
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def collect_environment_metadata(include_public_ip: bool = False) -> Dict[str, Optional[str]]:
|
|
39
|
+
"""Gather useful metadata (HWID only by default).
|
|
40
|
+
|
|
41
|
+
Note: Public IP is captured automatically by the server.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
metadata: Dict[str, Optional[str]] = {
|
|
45
|
+
"hwid": get_machine_fingerprint(),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Ces champs ne sont inclus que si explicitement demandé
|
|
49
|
+
# et nécessitent une configuration client_editable côté projet
|
|
50
|
+
if include_public_ip:
|
|
51
|
+
metadata["ip"] = get_public_ip()
|
|
52
|
+
|
|
53
|
+
return metadata
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
class AuthlixError(Exception):
|
|
2
|
+
"""Base class for SDK-specific exceptions."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class AuthenticationError(AuthlixError):
|
|
6
|
+
"""Raised when authentication fails or a token is missing."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ApiError(CyberLicensingError):
|
|
10
|
+
"""Raised when the API returns an error response."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, status_code: int, message: str, payload=None):
|
|
13
|
+
super().__init__(message)
|
|
14
|
+
self.status_code = status_code
|
|
15
|
+
self.payload = payload or {}
|
|
16
|
+
|
|
17
|
+
def __str__(self) -> str:
|
|
18
|
+
return f"API Error {self.status_code}: {super().__str__()}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BadRequestError(ApiError):
|
|
22
|
+
"""Raised when the API returns HTTP 400 (Bad Request)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str, payload=None):
|
|
25
|
+
super().__init__(400, message, payload)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ForbiddenError(ApiError):
|
|
29
|
+
"""Raised when the API returns HTTP 403 (Forbidden - insufficient project scope)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, payload=None):
|
|
32
|
+
super().__init__(403, message, payload)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class NotFoundError(ApiError):
|
|
36
|
+
"""Raised when the API returns HTTP 404 (Not Found)."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str, payload=None):
|
|
39
|
+
super().__init__(404, message, payload)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Manager-side client for Authlix operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
|
|
10
|
+
from .exceptions import ApiError, AuthenticationError, BadRequestError, ForbiddenError, NotFoundError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ManagerClient:
|
|
14
|
+
def __init__(self, base_url: str, token: Optional[str] = None, timeout: float = 5.0):
|
|
15
|
+
self.base_url = base_url.rstrip('/')
|
|
16
|
+
self.timeout = timeout
|
|
17
|
+
self.token = token
|
|
18
|
+
|
|
19
|
+
# --- Auth helpers ---
|
|
20
|
+
def authenticate(
|
|
21
|
+
self,
|
|
22
|
+
username: str,
|
|
23
|
+
*,
|
|
24
|
+
password: Optional[str] = None,
|
|
25
|
+
api_key: Optional[str] = None,
|
|
26
|
+
) -> str:
|
|
27
|
+
if not username:
|
|
28
|
+
raise AuthenticationError("username is required")
|
|
29
|
+
if (password is None and api_key is None) or (password is not None and api_key is not None):
|
|
30
|
+
raise AuthenticationError("Provide exactly one of password or api_key")
|
|
31
|
+
|
|
32
|
+
payload: Dict[str, Any] = {"username": username}
|
|
33
|
+
if password is not None:
|
|
34
|
+
payload["password"] = password
|
|
35
|
+
else:
|
|
36
|
+
payload["api_key"] = api_key
|
|
37
|
+
|
|
38
|
+
response = requests.post(
|
|
39
|
+
f"{self.base_url}/api/login",
|
|
40
|
+
json=payload,
|
|
41
|
+
timeout=self.timeout,
|
|
42
|
+
)
|
|
43
|
+
if response.status_code != 200:
|
|
44
|
+
message = "Invalid credentials"
|
|
45
|
+
try:
|
|
46
|
+
data = response.json()
|
|
47
|
+
except ValueError:
|
|
48
|
+
data = None
|
|
49
|
+
if isinstance(data, dict) and data.get("msg"):
|
|
50
|
+
message = data["msg"]
|
|
51
|
+
raise AuthenticationError(message)
|
|
52
|
+
token = response.json().get("access_token")
|
|
53
|
+
if not token:
|
|
54
|
+
raise AuthenticationError("Unable to retrieve access token")
|
|
55
|
+
self.token = token
|
|
56
|
+
return token
|
|
57
|
+
|
|
58
|
+
def _headers(self) -> Dict[str, str]:
|
|
59
|
+
if not self.token:
|
|
60
|
+
raise AuthenticationError("JWT token missing. Call authenticate() first or set token.")
|
|
61
|
+
return {"Authorization": f"Bearer {self.token}"}
|
|
62
|
+
|
|
63
|
+
def _request(self, method: str, path: str, require_auth: bool = True, **kwargs):
|
|
64
|
+
headers = kwargs.pop('headers', {})
|
|
65
|
+
if require_auth:
|
|
66
|
+
headers.update(self._headers())
|
|
67
|
+
response = requests.request(
|
|
68
|
+
method,
|
|
69
|
+
f"{self.base_url}{path}",
|
|
70
|
+
headers=headers,
|
|
71
|
+
timeout=self.timeout,
|
|
72
|
+
**kwargs,
|
|
73
|
+
)
|
|
74
|
+
if response.status_code >= 400:
|
|
75
|
+
payload = None
|
|
76
|
+
if response.content:
|
|
77
|
+
try:
|
|
78
|
+
payload = response.json()
|
|
79
|
+
except ValueError:
|
|
80
|
+
payload = None
|
|
81
|
+
message = (
|
|
82
|
+
payload.get("msg")
|
|
83
|
+
if isinstance(payload, dict) and payload.get("msg")
|
|
84
|
+
else response.text
|
|
85
|
+
)
|
|
86
|
+
# Raise specific exceptions for common error codes
|
|
87
|
+
if response.status_code == 400:
|
|
88
|
+
raise BadRequestError(message, payload)
|
|
89
|
+
elif response.status_code == 403:
|
|
90
|
+
raise ForbiddenError(message, payload)
|
|
91
|
+
elif response.status_code == 404:
|
|
92
|
+
raise NotFoundError(message, payload)
|
|
93
|
+
else:
|
|
94
|
+
raise ApiError(response.status_code, message, payload)
|
|
95
|
+
if response.content:
|
|
96
|
+
return response.json()
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# --- Project helpers ---
|
|
100
|
+
def list_projects(self) -> List[Dict]:
|
|
101
|
+
return self._request('GET', '/api/projects')
|
|
102
|
+
|
|
103
|
+
def create_project(self, name: str, description: Optional[str] = None) -> Dict:
|
|
104
|
+
payload: Dict[str, Any] = {"name": name}
|
|
105
|
+
if description is not None:
|
|
106
|
+
payload["description"] = description
|
|
107
|
+
return self._request('POST', '/api/projects', json=payload)
|
|
108
|
+
|
|
109
|
+
def add_project_manager(self, project_id: int, username: str) -> Dict:
|
|
110
|
+
payload = {"username": username}
|
|
111
|
+
return self._request('POST', f'/api/projects/{project_id}/add_manager', json=payload)
|
|
112
|
+
|
|
113
|
+
def update_metadata_schema(self, project_id: int, fields: List[Dict[str, Any]]) -> Dict:
|
|
114
|
+
return self._request('PUT', f'/api/projects/{project_id}/metadata_fields', json={"fields": fields})
|
|
115
|
+
|
|
116
|
+
def list_licenses(self, project_id: int) -> List[Dict]:
|
|
117
|
+
return self._request('GET', f'/api/projects/{project_id}/licenses')
|
|
118
|
+
|
|
119
|
+
def get_license_by_key(self, project_id: int, license_key: str) -> Dict:
|
|
120
|
+
"""Retrieve a single license by its key.
|
|
121
|
+
|
|
122
|
+
This is more efficient than fetching all licenses when you only need
|
|
123
|
+
to look up one license by its key. The key is automatically converted
|
|
124
|
+
to uppercase to avoid case mismatches.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
project_id:
|
|
129
|
+
The project ID that owns the license.
|
|
130
|
+
license_key:
|
|
131
|
+
The license key to look up (e.g., "CL-XXXX-XXXX").
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
Dict containing the license data.
|
|
136
|
+
|
|
137
|
+
Raises
|
|
138
|
+
------
|
|
139
|
+
BadRequestError:
|
|
140
|
+
If the key parameter is missing or invalid (HTTP 400).
|
|
141
|
+
ForbiddenError:
|
|
142
|
+
If the token doesn't have access to this project (HTTP 403).
|
|
143
|
+
NotFoundError:
|
|
144
|
+
If the license key doesn't exist in the project (HTTP 404).
|
|
145
|
+
"""
|
|
146
|
+
# Convert key to uppercase to avoid case mismatches
|
|
147
|
+
normalized_key = license_key.upper()
|
|
148
|
+
return self._request('GET', f'/api/projects/{project_id}/licenses/by_key?key={normalized_key}')
|
|
149
|
+
|
|
150
|
+
def create_license(
|
|
151
|
+
self,
|
|
152
|
+
project_id: int,
|
|
153
|
+
days_valid: Optional[int] = None,
|
|
154
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
155
|
+
) -> Dict:
|
|
156
|
+
payload: Dict[str, object] = {}
|
|
157
|
+
if days_valid is not None:
|
|
158
|
+
payload['days_valid'] = days_valid
|
|
159
|
+
if metadata is not None:
|
|
160
|
+
payload['metadata'] = metadata
|
|
161
|
+
return self._request('POST', f'/api/projects/{project_id}/licenses', json=payload)
|
|
162
|
+
|
|
163
|
+
def update_license(
|
|
164
|
+
self,
|
|
165
|
+
license_id: int,
|
|
166
|
+
*,
|
|
167
|
+
is_active: Optional[bool] = None,
|
|
168
|
+
expires_at: Optional[str] = None,
|
|
169
|
+
reset_hwid: bool = False,
|
|
170
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
171
|
+
) -> Dict:
|
|
172
|
+
payload: Dict[str, object] = {}
|
|
173
|
+
if is_active is not None:
|
|
174
|
+
payload['is_active'] = is_active
|
|
175
|
+
if expires_at is not None:
|
|
176
|
+
payload['expires_at'] = expires_at
|
|
177
|
+
if reset_hwid:
|
|
178
|
+
payload['reset_hwid'] = True
|
|
179
|
+
if metadata is not None:
|
|
180
|
+
payload['metadata'] = metadata
|
|
181
|
+
return self._request('PUT', f'/api/licenses/{license_id}', json=payload)
|
|
182
|
+
|
|
183
|
+
def extend_license(self, project_id: int, license_id: int, days: int) -> Dict:
|
|
184
|
+
"""Convenience helper to push expiration forward by *days*."""
|
|
185
|
+
|
|
186
|
+
licenses = self.list_licenses(project_id)
|
|
187
|
+
license_data = next((lic for lic in licenses if lic['id'] == license_id), None)
|
|
188
|
+
if not license_data:
|
|
189
|
+
raise ApiError(404, f"License {license_id} not found in project {project_id}", {})
|
|
190
|
+
|
|
191
|
+
expires_at = license_data.get('expires_at')
|
|
192
|
+
if not expires_at:
|
|
193
|
+
raise ApiError(400, "License is lifetime; set an explicit expiration first", {})
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
current_exp = datetime.fromisoformat(expires_at)
|
|
197
|
+
except ValueError as exc:
|
|
198
|
+
raise ApiError(500, f"Invalid expiration format: {expires_at}", {"error": str(exc)})
|
|
199
|
+
|
|
200
|
+
new_expiration = (current_exp + timedelta(days=days)).date().isoformat()
|
|
201
|
+
return self.update_license(license_id, expires_at=new_expiration)
|
|
202
|
+
|
|
203
|
+
def delete_license(self, license_id: int) -> Dict:
|
|
204
|
+
return self._request('DELETE', f'/api/licenses/{license_id}')
|
|
205
|
+
|
|
206
|
+
# --- API key helpers ---
|
|
207
|
+
def list_api_keys(self) -> List[Dict[str, Any]]:
|
|
208
|
+
return self._request('GET', '/api/api_keys')
|
|
209
|
+
|
|
210
|
+
def create_api_key(
|
|
211
|
+
self,
|
|
212
|
+
label: str,
|
|
213
|
+
*,
|
|
214
|
+
project_ids: Optional[List[int]] = None,
|
|
215
|
+
allow_all_projects: bool = False,
|
|
216
|
+
) -> Dict[str, Any]:
|
|
217
|
+
payload: Dict[str, Any] = {
|
|
218
|
+
"label": label,
|
|
219
|
+
"allow_all_projects": allow_all_projects,
|
|
220
|
+
}
|
|
221
|
+
if project_ids is not None:
|
|
222
|
+
payload["project_ids"] = project_ids
|
|
223
|
+
return self._request('POST', '/api/api_keys', json=payload)
|
|
224
|
+
|
|
225
|
+
def update_api_key(
|
|
226
|
+
self,
|
|
227
|
+
api_key_id: int,
|
|
228
|
+
*,
|
|
229
|
+
label: Optional[str] = None,
|
|
230
|
+
project_ids: Optional[List[int]] = None,
|
|
231
|
+
allow_all_projects: Optional[bool] = None,
|
|
232
|
+
) -> Dict[str, Any]:
|
|
233
|
+
payload: Dict[str, Any] = {}
|
|
234
|
+
if label is not None:
|
|
235
|
+
payload["label"] = label
|
|
236
|
+
if project_ids is not None:
|
|
237
|
+
payload["project_ids"] = project_ids
|
|
238
|
+
if allow_all_projects is not None:
|
|
239
|
+
payload["allow_all_projects"] = allow_all_projects
|
|
240
|
+
if not payload:
|
|
241
|
+
raise ValueError("At least one field must be provided to update an API key")
|
|
242
|
+
return self._request('PUT', f'/api/api_keys/{api_key_id}', json=payload)
|
|
243
|
+
|
|
244
|
+
def delete_api_key(self, api_key_id: int) -> Dict[str, Any]:
|
|
245
|
+
return self._request('DELETE', f'/api/api_keys/{api_key_id}')
|
|
246
|
+
|
|
247
|
+
def list_project_api_keys(self, project_id: int) -> List[Dict[str, Any]]:
|
|
248
|
+
return self._request('GET', f'/api/projects/{project_id}/api_keys')
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: authlix
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Client and manager helpers for Authlix
|
|
5
|
+
Author-email: Authlix <me@showdown.boo>
|
|
6
|
+
Project-URL: Homepage, https://authlix.io/
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: requests>=2.31.0
|
|
10
|
+
|
|
11
|
+
# Authlix Python SDK
|
|
12
|
+
|
|
13
|
+
> Client and manager helpers for integrating public validation flows and project operations with the Authlix API.
|
|
14
|
+
|
|
15
|
+
**License client** – validates license keys, collects environment metadata and lets end-users update `client_editable` fields.
|
|
16
|
+
**Manager client** – handles authentication plus CRUD operations for projects, metadata schemas and licenses.
|
|
17
|
+
**Environment helpers** – gather HWID, local/public IPs and other contextual information in one call.
|
|
18
|
+
|
|
19
|
+
The SDK targets Python 3.9+ and is published to PyPI as `authlix`.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install authlix
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
### Validate a license key
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from authlix import LicenseClient
|
|
35
|
+
|
|
36
|
+
client = LicenseClient(
|
|
37
|
+
base_url="https://authlix.io",
|
|
38
|
+
project_id=42,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Automatically collects HWID, LAN/public IP, etc.
|
|
42
|
+
result = client.validate_with_environment("CL-XXXX-XXXX")
|
|
43
|
+
print(result)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
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.
|
|
47
|
+
|
|
48
|
+
### Manage projects and licenses
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from authlix import ManagerClient
|
|
52
|
+
|
|
53
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
54
|
+
manager.authenticate("automation_bot", api_key="sk_live_XXXXXXXXXXXXXXXX")
|
|
55
|
+
|
|
56
|
+
# Create project and create license
|
|
57
|
+
project = manager.create_project(
|
|
58
|
+
name="My Awesome App",
|
|
59
|
+
description="Tooling pour la communauté",
|
|
60
|
+
)
|
|
61
|
+
license_data = manager.create_license(
|
|
62
|
+
project_id=project["id"],
|
|
63
|
+
days_valid=30,
|
|
64
|
+
metadata={"plan": "pro"},
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Revoke a license
|
|
68
|
+
manager.update_license(
|
|
69
|
+
license_id=license_data["id"],
|
|
70
|
+
is_active=False,
|
|
71
|
+
metadata={"plan": "revoked", "notes": "Chargeback"},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Update a license
|
|
75
|
+
manager.update_license(
|
|
76
|
+
license_id=license_data["id"],
|
|
77
|
+
metadata={"plan": "enterprise", "notes": "Upgraded"},
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Delegate day-to-day operations to another teammate
|
|
81
|
+
manager.add_project_manager(project_id=project["id"], username="support_team")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
More helpers are exposed for listing projects/licenses, extending expirations, resetting HWIDs, or deleting keys entirely.
|
|
85
|
+
|
|
86
|
+
### Look up a license by key
|
|
87
|
+
|
|
88
|
+
For efficiency, use `get_license_by_key` when you only need to look up a single license instead of fetching all licenses:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
try:
|
|
92
|
+
license_data = manager.get_license_by_key(
|
|
93
|
+
project_id=42,
|
|
94
|
+
license_key="CL-XXXX-XXXX"
|
|
95
|
+
)
|
|
96
|
+
print(f"License ID: {license_data['id']}, Active: {license_data['is_active']}")
|
|
97
|
+
except NotFoundError:
|
|
98
|
+
print("License key not found in this project")
|
|
99
|
+
except ForbiddenError:
|
|
100
|
+
print("Insufficient permissions for this project")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The key is automatically normalized to uppercase to avoid case mismatches. This method is ideal for dashboards and support tools that need to quickly retrieve license details by key.
|
|
104
|
+
|
|
105
|
+
### API key management
|
|
106
|
+
|
|
107
|
+
Automations or SDKs can provision scoped API keys and rotate them without touching end-user credentials.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
111
|
+
manager.authenticate("admin", password="password")
|
|
112
|
+
|
|
113
|
+
# Create a key limited to a project (secret is returned once)
|
|
114
|
+
api_key = manager.create_api_key(
|
|
115
|
+
label="CI Runner",
|
|
116
|
+
project_ids=[project["id"]],
|
|
117
|
+
allow_all_projects=False,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
# Update its scopes or friendly label
|
|
121
|
+
manager.update_api_key(
|
|
122
|
+
api_key_id=api_key["id"],
|
|
123
|
+
project_ids=[project["id"], 7],
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Dashboard helper: only show keys relevant to a project
|
|
127
|
+
manager.list_project_api_keys(project_id=project["id"])
|
|
128
|
+
|
|
129
|
+
# Cleanup when the automation is retired
|
|
130
|
+
manager.delete_api_key(api_key_id=api_key["id"])
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Use `manager.list_api_keys()` to render an operator-wide view (active + revoked keys).
|
|
134
|
+
|
|
135
|
+
## Client-editable metadata workflow
|
|
136
|
+
|
|
137
|
+
Certain metadata fields can be safely mutated by the end-user thanks to the `client_editable` flag defined in the project schema.
|
|
138
|
+
|
|
139
|
+
1. **Manager** defines the schema:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from authlix import ManagerClient
|
|
143
|
+
|
|
144
|
+
manager = ManagerClient(base_url="https://authlix.io")
|
|
145
|
+
manager.authenticate("admin", "password")
|
|
146
|
+
|
|
147
|
+
manager.update_metadata_schema(
|
|
148
|
+
project_id=42,
|
|
149
|
+
fields=[
|
|
150
|
+
{"name": "notes", "type": "string", "client_editable": True},
|
|
151
|
+
{"name": "plan", "type": "string", "client_editable": False},
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
2. **Client** updates the editable fields:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from authlix import LicenseClient
|
|
160
|
+
|
|
161
|
+
client = LicenseClient(base_url="https://authlix.io", project_id=42)
|
|
162
|
+
|
|
163
|
+
client.update_client_metadata(
|
|
164
|
+
key="CL-XXXX-XXXX",
|
|
165
|
+
metadata={"notes": "Nouvelle machine"},
|
|
166
|
+
)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
`update_client_metadata` validates locally that `project_id` is an `int` and the `metadata` dictionary is non-empty before calling `POST /api/client_metadata`. Server responses bubble up any JSON `msg` errors through `ApiError` for easier debugging (e.g., non editable field, inactive key, etc.).
|
|
170
|
+
|
|
171
|
+
## Environment helpers
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from authlix import collect_environment_metadata
|
|
175
|
+
|
|
176
|
+
print(collect_environment_metadata())
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The helper returns HWID, hostname, LAN IP, optional public IP, and other useful attributes. Use it to enrich `validate_license` calls or to store telemetry on validated machines.
|
|
180
|
+
|
|
181
|
+
## Error handling
|
|
182
|
+
|
|
183
|
+
The SDK provides specific exception classes for common HTTP errors:
|
|
184
|
+
|
|
185
|
+
- `BadRequestError` – raised for HTTP 400 (Bad Request, e.g., missing or invalid parameters)
|
|
186
|
+
- `ForbiddenError` – raised for HTTP 403 (Forbidden, e.g., insufficient project scope)
|
|
187
|
+
- `NotFoundError` – raised for HTTP 404 (Not Found, e.g., unknown license key)
|
|
188
|
+
- `ApiError(status_code, message, payload)` – generic error for other HTTP ≥ 400 responses
|
|
189
|
+
- `AuthenticationError` – surfaces authentication/authorization issues in manager flows
|
|
190
|
+
- `AuthlixError` – base class for all SDK exceptions for easy broad exception handling
|
|
191
|
+
|
|
192
|
+
All error classes include a `status_code` and `payload` for detailed debugging:
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from authlix import ApiError, BadRequestError, ForbiddenError, NotFoundError
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
license_data = manager.get_license_by_key(project_id=42, license_key="CL-XXXX")
|
|
199
|
+
except BadRequestError as exc:
|
|
200
|
+
print(f"Invalid request: {exc}")
|
|
201
|
+
except ForbiddenError as exc:
|
|
202
|
+
print(f"Access denied to project: {exc}")
|
|
203
|
+
except NotFoundError as exc:
|
|
204
|
+
print(f"License not found: {exc}")
|
|
205
|
+
except ApiError as exc:
|
|
206
|
+
print(f"API error {exc.status_code}: {exc}")
|
|
207
|
+
print(exc.payload)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Local development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
python -m venv .venv
|
|
214
|
+
.venv\Scripts\activate
|
|
215
|
+
pip install -e .
|
|
216
|
+
python -m build
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Additional helpers:
|
|
220
|
+
|
|
221
|
+
- `list_licenses(project_id)` – list all licenses (keys + live metadata and usage counters) for a project (prefer `get_license_by_key` for single lookups)
|
|
222
|
+
- `get_license_by_key(project_id, license_key)` – efficiently retrieve a single license by its key (automatically normalized to uppercase)
|
|
223
|
+
- `create_license(project_id, days_valid=None, metadata=None)` – generate a new license key
|
|
224
|
+
- `update_license(license_id, is_active=None, expires_at=None, reset_hwid=False, metadata=None)` – ban/disable, move expiration, reset HWID, or overwrite metadata
|
|
225
|
+
- `extend_license(project_id, license_id, days)` – convenience method to push expiration forward
|
|
226
|
+
- `delete_license(license_id)` – hard-delete a key
|
|
227
|
+
- `add_project_manager(project_id, username)` – delegate back-office access to teammates
|
|
228
|
+
- `create_api_key(label, project_ids=None, allow_all_projects=False)` – mint scoped API keys from the dashboard
|
|
229
|
+
- `update_api_key(api_key_id, ...)`/`delete_api_key(api_key_id)` – rotate or revoke keys instantly
|
|
230
|
+
- `list_api_keys()`/`list_project_api_keys(project_id)` – audit keys globally or per project
|
|
231
|
+
|
|
232
|
+
## Environment helpers
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
from authlix import collect_environment_metadata
|
|
236
|
+
|
|
237
|
+
print(collect_environment_metadata())
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Returns HWID, hostname, LAN IP, and (if available) public IP.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
authlix/__init__.py
|
|
4
|
+
authlix/client.py
|
|
5
|
+
authlix/environment.py
|
|
6
|
+
authlix/exceptions.py
|
|
7
|
+
authlix/manager.py
|
|
8
|
+
authlix.egg-info/PKG-INFO
|
|
9
|
+
authlix.egg-info/SOURCES.txt
|
|
10
|
+
authlix.egg-info/dependency_links.txt
|
|
11
|
+
authlix.egg-info/requires.txt
|
|
12
|
+
authlix.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
requests>=2.31.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
authlix
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "authlix"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Client and manager helpers for Authlix"
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Authlix", email = "me@showdown.boo" },
|
|
11
|
+
]
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
dependencies = [
|
|
15
|
+
"requests>=2.31.0",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.urls]
|
|
19
|
+
Homepage = "https://authlix.io/"
|
authlix-0.3.0/setup.cfg
ADDED