authforge-sdk 1.0.2__tar.gz → 1.0.3__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.
- {authforge_sdk-1.0.2/authforge_sdk.egg-info → authforge_sdk-1.0.3}/PKG-INFO +5 -2
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/README.md +4 -1
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/authforge.py +89 -18
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3/authforge_sdk.egg-info}/PKG-INFO +5 -2
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/pyproject.toml +1 -1
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/LICENSE +0 -0
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/authforge_sdk.egg-info/SOURCES.txt +0 -0
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/authforge_sdk.egg-info/dependency_links.txt +0 -0
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/authforge_sdk.egg-info/requires.txt +0 -0
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/authforge_sdk.egg-info/top_level.txt +0 -0
- {authforge_sdk-1.0.2 → authforge_sdk-1.0.3}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Official Python SDK for AuthForge — credit-based license key authentication with Ed25519-verified responses.
|
|
5
5
|
Author: AuthForge
|
|
6
6
|
License: MIT
|
|
@@ -94,7 +94,7 @@ client = AuthForgeClient(
|
|
|
94
94
|
|
|
95
95
|
## Billing
|
|
96
96
|
|
|
97
|
-
- **1 `login()` call = 1 credit** (one `/auth/validate` debit).
|
|
97
|
+
- **1 `login()` or `validate_license()` call = 1 credit** (one `/auth/validate` debit each).
|
|
98
98
|
- **10 heartbeats on the same license = 1 credit** (billed every 10th successful heartbeat).
|
|
99
99
|
|
|
100
100
|
A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A server app running 24/7 at a 1-minute interval burns ~145 credits/day — pick the interval based on how fast you need revocations to propagate (they always land on the **next** heartbeat).
|
|
@@ -104,6 +104,7 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
104
104
|
| Method | Returns | Description |
|
|
105
105
|
|---|---|---|
|
|
106
106
|
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
107
|
+
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start heartbeats; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
107
108
|
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
108
109
|
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
|
|
109
110
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
@@ -121,6 +122,8 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
121
122
|
|
|
122
123
|
If authentication fails (login rejected, heartbeat fails, signature mismatch, etc.), the SDK calls your `on_failure` callback if one is provided. If no callback is set, **the SDK calls `os._exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
|
|
123
124
|
|
|
125
|
+
**`validate_license()`** does not trigger `on_failure` or `os._exit` — check `result["valid"]` and `result["code"]`.
|
|
126
|
+
|
|
124
127
|
Recognized server errors:
|
|
125
128
|
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `revoke_requires_session`, `bad_request`
|
|
126
129
|
|
|
@@ -67,7 +67,7 @@ client = AuthForgeClient(
|
|
|
67
67
|
|
|
68
68
|
## Billing
|
|
69
69
|
|
|
70
|
-
- **1 `login()` call = 1 credit** (one `/auth/validate` debit).
|
|
70
|
+
- **1 `login()` or `validate_license()` call = 1 credit** (one `/auth/validate` debit each).
|
|
71
71
|
- **10 heartbeats on the same license = 1 credit** (billed every 10th successful heartbeat).
|
|
72
72
|
|
|
73
73
|
A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A server app running 24/7 at a 1-minute interval burns ~145 credits/day — pick the interval based on how fast you need revocations to propagate (they always land on the **next** heartbeat).
|
|
@@ -77,6 +77,7 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
77
77
|
| Method | Returns | Description |
|
|
78
78
|
|---|---|---|
|
|
79
79
|
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
80
|
+
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start heartbeats; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
80
81
|
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
81
82
|
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
|
|
82
83
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
@@ -94,6 +95,8 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
94
95
|
|
|
95
96
|
If authentication fails (login rejected, heartbeat fails, signature mismatch, etc.), the SDK calls your `on_failure` callback if one is provided. If no callback is set, **the SDK calls `os._exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
|
|
96
97
|
|
|
98
|
+
**`validate_license()`** does not trigger `on_failure` or `os._exit` — check `result["valid"]` and `result["code"]`.
|
|
99
|
+
|
|
97
100
|
Recognized server errors:
|
|
98
101
|
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `revoke_requires_session`, `bad_request`
|
|
99
102
|
|
|
@@ -11,7 +11,7 @@ import time
|
|
|
11
11
|
import urllib.error
|
|
12
12
|
import urllib.request
|
|
13
13
|
import uuid
|
|
14
|
-
from typing import Any, Callable, Dict, Optional
|
|
14
|
+
from typing import Any, Callable, Dict, Literal, Optional, TypedDict, Union
|
|
15
15
|
from cryptography.exceptions import InvalidSignature
|
|
16
16
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
17
17
|
|
|
@@ -19,6 +19,25 @@ from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
|
19
19
|
DEFAULT_API_BASE_URL = "https://auth.authforge.cc"
|
|
20
20
|
RATE_LIMIT_RETRY_DELAYS = (2, 5)
|
|
21
21
|
NETWORK_RETRY_DELAY = 2
|
|
22
|
+
class ValidateLicenseSuccess(TypedDict):
|
|
23
|
+
valid: Literal[True]
|
|
24
|
+
session_token: str
|
|
25
|
+
expires_in: int
|
|
26
|
+
session_data: Dict[str, Any]
|
|
27
|
+
app_variables: Optional[Dict[str, Any]]
|
|
28
|
+
license_variables: Optional[Dict[str, Any]]
|
|
29
|
+
key_id: Optional[str]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ValidateLicenseFailure(TypedDict):
|
|
33
|
+
valid: Literal[False]
|
|
34
|
+
code: str
|
|
35
|
+
error: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
ValidateLicenseResult = Union[ValidateLicenseSuccess, ValidateLicenseFailure]
|
|
39
|
+
|
|
40
|
+
|
|
22
41
|
KNOWN_SERVER_ERRORS = {
|
|
23
42
|
"invalid_app",
|
|
24
43
|
"invalid_key",
|
|
@@ -109,6 +128,36 @@ class AuthForgeClient:
|
|
|
109
128
|
self._fail("login_failed", exc)
|
|
110
129
|
return False
|
|
111
130
|
|
|
131
|
+
def validate_license(self, license_key: str) -> ValidateLicenseResult:
|
|
132
|
+
"""Validate like :meth:`login` (same /auth/validate + signatures) without storing
|
|
133
|
+
session state or starting the heartbeat thread."""
|
|
134
|
+
if not license_key or not isinstance(license_key, str):
|
|
135
|
+
raise ValueError("license_key must be a non-empty string")
|
|
136
|
+
try:
|
|
137
|
+
body: Dict[str, Any] = {
|
|
138
|
+
"appId": self.app_id,
|
|
139
|
+
"appSecret": self.app_secret,
|
|
140
|
+
"licenseKey": license_key,
|
|
141
|
+
"hwid": self._hwid,
|
|
142
|
+
"nonce": self._generate_nonce(),
|
|
143
|
+
}
|
|
144
|
+
if self.ttl_seconds is not None:
|
|
145
|
+
body["ttlSeconds"] = self.ttl_seconds
|
|
146
|
+
response_obj = self._post_json("/auth/validate", body, skip_failure_hook=True)
|
|
147
|
+
expected_nonce = str(body.get("nonce", "")).strip()
|
|
148
|
+
parsed = self._parse_validate_success(response_obj, expected_nonce)
|
|
149
|
+
return {
|
|
150
|
+
"valid": True,
|
|
151
|
+
"session_token": parsed["session_token"],
|
|
152
|
+
"expires_in": parsed["expires_in"],
|
|
153
|
+
"session_data": parsed["session_data"],
|
|
154
|
+
"app_variables": parsed["app_variables"],
|
|
155
|
+
"license_variables": parsed["license_variables"],
|
|
156
|
+
"key_id": parsed["key_id"],
|
|
157
|
+
}
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
return {"valid": False, "code": str(exc), "error": str(exc)}
|
|
160
|
+
|
|
112
161
|
def self_ban(
|
|
113
162
|
self,
|
|
114
163
|
*,
|
|
@@ -250,13 +299,9 @@ class AuthForgeClient:
|
|
|
250
299
|
context="validate",
|
|
251
300
|
)
|
|
252
301
|
|
|
253
|
-
def
|
|
254
|
-
self,
|
|
255
|
-
|
|
256
|
-
expected_nonce: str,
|
|
257
|
-
license_key: Optional[str],
|
|
258
|
-
context: str,
|
|
259
|
-
) -> None:
|
|
302
|
+
def _parse_validate_success(
|
|
303
|
+
self, response_obj: Dict[str, Any], expected_nonce: str
|
|
304
|
+
) -> Dict[str, Any]:
|
|
260
305
|
status = response_obj.get("status")
|
|
261
306
|
if not self._is_success_status(status):
|
|
262
307
|
error_code = self._extract_server_error(response_obj)
|
|
@@ -288,21 +333,46 @@ class AuthForgeClient:
|
|
|
288
333
|
if expires_in is None:
|
|
289
334
|
raise ValueError("missing_expiresIn")
|
|
290
335
|
|
|
336
|
+
return {
|
|
337
|
+
"session_token": session_token,
|
|
338
|
+
"expires_in": int(expires_in),
|
|
339
|
+
"session_data": dict(payload_json),
|
|
340
|
+
"app_variables": self._extract_optional_map(payload_json.get("appVariables")),
|
|
341
|
+
"license_variables": self._extract_optional_map(
|
|
342
|
+
payload_json.get("licenseVariables")
|
|
343
|
+
),
|
|
344
|
+
"key_id": key_id if isinstance(key_id, str) else None,
|
|
345
|
+
"raw_payload_b64": raw_payload_b64,
|
|
346
|
+
"signature": signature,
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
def _apply_signed_response(
|
|
350
|
+
self,
|
|
351
|
+
response_obj: Dict[str, Any],
|
|
352
|
+
expected_nonce: str,
|
|
353
|
+
license_key: Optional[str],
|
|
354
|
+
context: str,
|
|
355
|
+
) -> None:
|
|
356
|
+
parsed = self._parse_validate_success(response_obj, expected_nonce)
|
|
357
|
+
_ = context
|
|
358
|
+
|
|
291
359
|
with self._lock:
|
|
292
360
|
if license_key is not None:
|
|
293
361
|
self._license_key = license_key
|
|
294
|
-
self._session_token = session_token
|
|
295
|
-
self._session_expires_in = int(expires_in)
|
|
362
|
+
self._session_token = parsed["session_token"]
|
|
363
|
+
self._session_expires_in = int(parsed["expires_in"])
|
|
296
364
|
self._last_nonce = expected_nonce
|
|
297
|
-
self._raw_payload_b64 = raw_payload_b64
|
|
298
|
-
self._signature = signature
|
|
299
|
-
self._key_id = key_id
|
|
300
|
-
self._session_data = dict(
|
|
301
|
-
self._app_variables =
|
|
302
|
-
self._license_variables =
|
|
365
|
+
self._raw_payload_b64 = parsed["raw_payload_b64"]
|
|
366
|
+
self._signature = parsed["signature"]
|
|
367
|
+
self._key_id = parsed["key_id"]
|
|
368
|
+
self._session_data = dict(parsed["session_data"])
|
|
369
|
+
self._app_variables = parsed["app_variables"]
|
|
370
|
+
self._license_variables = parsed["license_variables"]
|
|
303
371
|
self._authenticated = True
|
|
304
372
|
|
|
305
|
-
def _post_json(
|
|
373
|
+
def _post_json(
|
|
374
|
+
self, path: str, data: Dict[str, Any], *, skip_failure_hook: bool = False
|
|
375
|
+
) -> Dict[str, Any]:
|
|
306
376
|
url = f"{self.api_base_url}{path}"
|
|
307
377
|
body = dict(data)
|
|
308
378
|
rate_attempt = 0
|
|
@@ -342,7 +412,8 @@ class AuthForgeClient:
|
|
|
342
412
|
network_attempt += 1
|
|
343
413
|
time.sleep(NETWORK_RETRY_DELAY)
|
|
344
414
|
continue
|
|
345
|
-
|
|
415
|
+
if not skip_failure_hook:
|
|
416
|
+
self._fail("network_error", exc)
|
|
346
417
|
raise RuntimeError(f"url_error: {exc}") from exc
|
|
347
418
|
|
|
348
419
|
is_rate_limited = (
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Official Python SDK for AuthForge — credit-based license key authentication with Ed25519-verified responses.
|
|
5
5
|
Author: AuthForge
|
|
6
6
|
License: MIT
|
|
@@ -94,7 +94,7 @@ client = AuthForgeClient(
|
|
|
94
94
|
|
|
95
95
|
## Billing
|
|
96
96
|
|
|
97
|
-
- **1 `login()` call = 1 credit** (one `/auth/validate` debit).
|
|
97
|
+
- **1 `login()` or `validate_license()` call = 1 credit** (one `/auth/validate` debit each).
|
|
98
98
|
- **10 heartbeats on the same license = 1 credit** (billed every 10th successful heartbeat).
|
|
99
99
|
|
|
100
100
|
A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A server app running 24/7 at a 1-minute interval burns ~145 credits/day — pick the interval based on how fast you need revocations to propagate (they always land on the **next** heartbeat).
|
|
@@ -104,6 +104,7 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
104
104
|
| Method | Returns | Description |
|
|
105
105
|
|---|---|---|
|
|
106
106
|
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
107
|
+
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start heartbeats; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
107
108
|
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
108
109
|
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
|
|
109
110
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
@@ -121,6 +122,8 @@ A desktop app running 6h/day at a 15-minute interval burns ~3–4 credits/day. A
|
|
|
121
122
|
|
|
122
123
|
If authentication fails (login rejected, heartbeat fails, signature mismatch, etc.), the SDK calls your `on_failure` callback if one is provided. If no callback is set, **the SDK calls `os._exit(1)` to terminate the process.** This is intentional — it prevents your app from running without a valid license.
|
|
123
124
|
|
|
125
|
+
**`validate_license()`** does not trigger `on_failure` or `os._exit` — check `result["valid"]` and `result["code"]`.
|
|
126
|
+
|
|
124
127
|
Recognized server errors:
|
|
125
128
|
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `revoke_requires_session`, `bad_request`
|
|
126
129
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "authforge-sdk"
|
|
7
|
-
version = "1.0.
|
|
7
|
+
version = "1.0.3"
|
|
8
8
|
description = "Official Python SDK for AuthForge — credit-based license key authentication with Ed25519-verified responses."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|