authforge-sdk 1.5.0__tar.gz → 2.0.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.
- {authforge_sdk-1.5.0/authforge_sdk.egg-info → authforge_sdk-2.0.0}/PKG-INFO +27 -13
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/README.md +26 -12
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/authforge.py +108 -11
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0/authforge_sdk.egg-info}/PKG-INFO +27 -13
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/pyproject.toml +1 -1
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/LICENSE +0 -0
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/authforge_sdk.egg-info/SOURCES.txt +0 -0
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/authforge_sdk.egg-info/dependency_links.txt +0 -0
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/authforge_sdk.egg-info/requires.txt +0 -0
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/authforge_sdk.egg-info/top_level.txt +0 -0
- {authforge_sdk-1.5.0 → authforge_sdk-2.0.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
@@ -52,7 +52,7 @@ Everything in this list ships in `authforge.py` today:
|
|
|
52
52
|
- **Offline license files (`.authforge`)**: `login_from_file()` / `verify_license_file()` verify a cloud-minted, Ed25519-signed file with zero network access for air-gapped machines.
|
|
53
53
|
- **Self-serve offline activation**: `activate_offline()` turns a license key into a lifetime, HWID-bound `.authforge` file with one online call, then runs offline from that file.
|
|
54
54
|
- **Nonce anti-replay**: a fresh 128-bit nonce is sent on every request and the echoed nonce in the signed payload is checked before the response is accepted.
|
|
55
|
-
- **HWID
|
|
55
|
+
- **HWID v2**: per-app device id derived from the OS machine id (macOS `IOPlatformUUID`, Linux `/etc/machine-id`, Windows `MachineGuid`), identical across all AuthForge SDKs for the same app and machine.
|
|
56
56
|
- **`hwid_override`**: bind to any identity instead of the machine (for example `tg:<id>`, `discord:<id>`).
|
|
57
57
|
- **Seat enforcement**: the server binds each HWID into a license's free slots up to `maxHwidSlots`; `hwid_count` / `max_hwid_slots` are surfaced on the result. A shared (unlimited-seat) key skips per-device binding.
|
|
58
58
|
- **Self-ban** (`self_ban()`) for anti-tamper response, both pre-session and post-session.
|
|
@@ -119,7 +119,7 @@ client = AuthForgeClient(
|
|
|
119
119
|
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
|
|
120
120
|
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
|
|
121
121
|
| `ttl_seconds` | `int \| None` | `None` (server default: 86400) | The grace period duration: how long the app keeps running on the signed session without contacting AuthForge. Server clamps to `[3600, 604800]` (1h to 7d); preserved across check-in refreshes. |
|
|
122
|
-
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of machine
|
|
122
|
+
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of deriving the HWID from the OS machine id. |
|
|
123
123
|
| `online_heartbeat` | bool (keyword-only) | `False` | Enable online check-ins: periodic `POST /auth/heartbeat` for fast revocation and concurrent-use detection. |
|
|
124
124
|
|
|
125
125
|
### Identity-based binding example (Telegram/Discord)
|
|
@@ -190,7 +190,7 @@ if client.login_from_file("license.authforge"):
|
|
|
190
190
|
print(client.get_license_variables())
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
Since 2.0 every AuthForge SDK computes the same HWID for the same app id and machine, but HWIDs collected before upgrading to 2.0 no longer match: collect the HWID again after upgrading. After `login_from_file()`, `get_session_kind()` returns `"offline"` (`"online"` after `login()`, `None` when logged out).
|
|
194
194
|
|
|
195
195
|
`verify_license_file()` (module function and client method) performs the same checks without touching client state. Failure codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `login_from_file()` reports them through `on_failure("offline_login_failed", exc)` and returns `False`; it never calls `os._exit`.
|
|
196
196
|
|
|
@@ -297,7 +297,7 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
297
297
|
|---|---|---|
|
|
298
298
|
| `login(license_key)` | `bool` | Activates: validates the key online and stores the signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
299
299
|
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start background checks; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
300
|
-
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
300
|
+
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only). Raises `ValueError` on any failure (a network failure as `AuthForgeError` `network_error` / `timeout`); never calls `on_failure` or `os._exit` |
|
|
301
301
|
| `login_from_file(path_or_text)` | `bool` | Authorizes from an offline `.authforge` file with no network; never starts background checks; failures go to `on_failure("offline_login_failed", …)` |
|
|
302
302
|
| `activate_offline(license_key, path=None)` | `bool` | Self-serve offline activation: one online call returns a lifetime HWID-bound `.authforge` file, verified locally, written atomically to `path` and applied like `login_from_file`. Needs the App Secret; raises `AuthForgeError` on refusal; never calls `on_failure` or `os._exit` |
|
|
303
303
|
| `verify_license_file(path_or_text, *, now=None)` | `VerifyLicenseFileResult` | Verifies a `.authforge` file with this client's app id / keys / HWID without changing state |
|
|
@@ -323,6 +323,8 @@ Recognized server errors (`KNOWN_SERVER_ERRORS`):
|
|
|
323
323
|
|
|
324
324
|
**`activate_offline()`** does not trigger `on_failure` or `os._exit` either: it raises `AuthForgeError` (see [Self-serve offline activation](#self-serve-offline-activation)).
|
|
325
325
|
|
|
326
|
+
**`self_ban()`** does not trigger `on_failure` or `os._exit` either: every failure raises, a server rejection as `ValueError(code)` and a network failure as `AuthForgeError` with `exc.code` `network_error` or `timeout`.
|
|
327
|
+
|
|
326
328
|
Request retries are automatic inside the internal HTTP layer:
|
|
327
329
|
- `rate_limited`, or HTTP 429 with no error code: retry after 2s, then 5s (max 3 attempts total)
|
|
328
330
|
- `no_credits`, `demo_quota_exceeded` and `app_burn_cap_reached` also arrive as HTTP 429 but are never retried immediately; online check-ins try again at the next interval
|
|
@@ -343,7 +345,7 @@ A failed check-in only counts as an AuthForge verdict when the body is a JSON ob
|
|
|
343
345
|
| Fatal | `revoked`, `expired`, `hwid_mismatch` (the HWID is no longer bound to the license, for example after an HWID reset), `blocked` (HWID/IP blacklisted or not whitelisted), `session_expired` (also raised locally when the grace period or session TTL runs out), `malformed_request`, `app_disabled`, `invalid_app`, `signature_mismatch` (plus the `activate_offline()`-only codes `offline_activation_*` and `offline_file_rejected`) | Clears the stored session (as `logout()` does) and stops background checks **before** calling `on_failure`, so the grace period cannot keep the app running on it. `on_failure` may call `login()` again. |
|
|
344
346
|
| Transient | Everything else: `network_error`, `timeout`, `rate_limited`, `system_error`, `no_credits`, `demo_quota_exceeded`, `app_burn_cap_reached`, `bad_request`, `invalid_key`, every `http_error_<status>`, `invalid_json_response`, `unexpected_response`, `nonce_mismatch`, any unrecognized code | Keeps the session and checks in again on the next `heartbeat_interval`. Once the signed session's TTL has passed, the next transient failure is reported as a fatal `session_expired` instead. |
|
|
345
347
|
|
|
346
|
-
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`.
|
|
348
|
+
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`. Network failures are reported once, with code `network_error` or `timeout`: as `heartbeat_failed` from check-ins and as `login_failed` from `login()`, never as a separate `network_error` reason. `activate_offline()` and `self_ban()` raise them as `AuthForgeError` instead.
|
|
347
349
|
|
|
348
350
|
`on_failure` runs on the background heartbeat thread with no SDK lock held, so calling `logout()`, `is_authenticated()` or `login()` from it is safe. A check-in that is still in flight when you call `logout()` or `login()` is discarded: it cannot restore the old session or report a failure for it.
|
|
349
351
|
|
|
@@ -413,11 +415,12 @@ client.self_ban(
|
|
|
413
415
|
- Uses post-session mode when a session token is available (`session_token` arg or current SDK session).
|
|
414
416
|
- Falls back to pre-session mode using `license_key` + nonce + app secret.
|
|
415
417
|
- In pre-session mode, revoke is forced off client-side to avoid unsafe key revocations.
|
|
418
|
+
- Every failure raises: a server rejection as `ValueError(code)`, a network failure (after one retry) as `AuthForgeError` with `exc.code` `network_error` or `timeout`. `self_ban()` never calls `on_failure` or `os._exit`, with or without a callback.
|
|
416
419
|
- Not available after `login_from_file()`: offline sessions have no server session, so `self_ban()` with no explicit `license_key` / `session_token` raises `ValueError("offline_session")` without contacting the server.
|
|
417
420
|
|
|
418
421
|
## How It Works
|
|
419
422
|
|
|
420
|
-
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it
|
|
423
|
+
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it derives the HWID from the OS machine id (see [Hardware ID](#hardware-id)). It then generates a random nonce and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload with a TTL. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
|
|
421
424
|
|
|
422
425
|
2. **Background checks**: a daemon thread wakes at the configured interval. By default it enforces the grace period: it re-verifies the stored signature and checks expiry without network calls. With `online_heartbeat=True`, it instead sends `/auth/heartbeat` with a fresh nonce and verifies the response.
|
|
423
426
|
|
|
@@ -425,18 +428,29 @@ client.self_ban(
|
|
|
425
428
|
|
|
426
429
|
## Hardware ID
|
|
427
430
|
|
|
428
|
-
The
|
|
429
|
-
|
|
430
|
-
-
|
|
431
|
-
|
|
431
|
+
The HWID (v2, since 2.0) is
|
|
432
|
+
|
|
433
|
+
`SHA256("authforge-hwid-v2|" + app_id + "|" + machine_id)` as lowercase hex,
|
|
434
|
+
|
|
435
|
+
where `machine_id` is the operating system's own machine id, trimmed and lowercased:
|
|
436
|
+
|
|
437
|
+
| OS | Source |
|
|
438
|
+
| -- | ------ |
|
|
439
|
+
| macOS | `IOPlatformUUID` (`/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice`) |
|
|
440
|
+
| Linux | `/etc/machine-id`, then `/var/lib/dbus/machine-id` |
|
|
441
|
+
| Windows | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (64-bit registry view, via `winreg`) |
|
|
442
|
+
|
|
443
|
+
These ids survive network changes, renames, disk usage and USB devices. Including the app id means two different apps on the same machine get unrelated HWIDs. Every AuthForge SDK computes the same value for the same app id and machine; `hwid_vectors.json` pins this. The HWID is sent with every auth request so the server can enforce per-device license limits.
|
|
444
|
+
|
|
445
|
+
If no valid OS id can be read (for example a container without `/etc/machine-id`), the SDK falls back to `legacy:<the pre-2.0 fingerprint of MAC, CPU and disk serial>` as the machine id. Containers built from one image can also share a baked-in machine id, so for containers prefer `hwid_override` with an identifier you control.
|
|
432
446
|
|
|
433
|
-
|
|
447
|
+
The helpers `hwid_from_machine_id`, `normalize_machine_id`, `parse_ioreg_platform_uuid` and `parse_reg_query_machine_guid` are exported for tooling and tests.
|
|
434
448
|
|
|
435
449
|
For non-device identities (for example Telegram users), pass `hwid_override` such as `tg:<user_id>`.
|
|
436
450
|
|
|
437
451
|
## Test Vectors
|
|
438
452
|
|
|
439
|
-
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code.
|
|
453
|
+
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code. `hwid_vectors.json` (generated by `authforge-node/generate_hwid_vectors.mjs`) is the cross-SDK suite for HWID v2: derivation, machine id normalization, and the `ioreg` / `reg query` parsers.
|
|
440
454
|
|
|
441
455
|
## Requirements
|
|
442
456
|
|
|
@@ -24,7 +24,7 @@ Everything in this list ships in `authforge.py` today:
|
|
|
24
24
|
- **Offline license files (`.authforge`)**: `login_from_file()` / `verify_license_file()` verify a cloud-minted, Ed25519-signed file with zero network access for air-gapped machines.
|
|
25
25
|
- **Self-serve offline activation**: `activate_offline()` turns a license key into a lifetime, HWID-bound `.authforge` file with one online call, then runs offline from that file.
|
|
26
26
|
- **Nonce anti-replay**: a fresh 128-bit nonce is sent on every request and the echoed nonce in the signed payload is checked before the response is accepted.
|
|
27
|
-
- **HWID
|
|
27
|
+
- **HWID v2**: per-app device id derived from the OS machine id (macOS `IOPlatformUUID`, Linux `/etc/machine-id`, Windows `MachineGuid`), identical across all AuthForge SDKs for the same app and machine.
|
|
28
28
|
- **`hwid_override`**: bind to any identity instead of the machine (for example `tg:<id>`, `discord:<id>`).
|
|
29
29
|
- **Seat enforcement**: the server binds each HWID into a license's free slots up to `maxHwidSlots`; `hwid_count` / `max_hwid_slots` are surfaced on the result. A shared (unlimited-seat) key skips per-device binding.
|
|
30
30
|
- **Self-ban** (`self_ban()`) for anti-tamper response, both pre-session and post-session.
|
|
@@ -91,7 +91,7 @@ client = AuthForgeClient(
|
|
|
91
91
|
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
|
|
92
92
|
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
|
|
93
93
|
| `ttl_seconds` | `int \| None` | `None` (server default: 86400) | The grace period duration: how long the app keeps running on the signed session without contacting AuthForge. Server clamps to `[3600, 604800]` (1h to 7d); preserved across check-in refreshes. |
|
|
94
|
-
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of machine
|
|
94
|
+
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of deriving the HWID from the OS machine id. |
|
|
95
95
|
| `online_heartbeat` | bool (keyword-only) | `False` | Enable online check-ins: periodic `POST /auth/heartbeat` for fast revocation and concurrent-use detection. |
|
|
96
96
|
|
|
97
97
|
### Identity-based binding example (Telegram/Discord)
|
|
@@ -162,7 +162,7 @@ if client.login_from_file("license.authforge"):
|
|
|
162
162
|
print(client.get_license_variables())
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Since 2.0 every AuthForge SDK computes the same HWID for the same app id and machine, but HWIDs collected before upgrading to 2.0 no longer match: collect the HWID again after upgrading. After `login_from_file()`, `get_session_kind()` returns `"offline"` (`"online"` after `login()`, `None` when logged out).
|
|
166
166
|
|
|
167
167
|
`verify_license_file()` (module function and client method) performs the same checks without touching client state. Failure codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `login_from_file()` reports them through `on_failure("offline_login_failed", exc)` and returns `False`; it never calls `os._exit`.
|
|
168
168
|
|
|
@@ -269,7 +269,7 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
269
269
|
|---|---|---|
|
|
270
270
|
| `login(license_key)` | `bool` | Activates: validates the key online and stores the signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
271
271
|
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start background checks; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
272
|
-
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
272
|
+
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only). Raises `ValueError` on any failure (a network failure as `AuthForgeError` `network_error` / `timeout`); never calls `on_failure` or `os._exit` |
|
|
273
273
|
| `login_from_file(path_or_text)` | `bool` | Authorizes from an offline `.authforge` file with no network; never starts background checks; failures go to `on_failure("offline_login_failed", …)` |
|
|
274
274
|
| `activate_offline(license_key, path=None)` | `bool` | Self-serve offline activation: one online call returns a lifetime HWID-bound `.authforge` file, verified locally, written atomically to `path` and applied like `login_from_file`. Needs the App Secret; raises `AuthForgeError` on refusal; never calls `on_failure` or `os._exit` |
|
|
275
275
|
| `verify_license_file(path_or_text, *, now=None)` | `VerifyLicenseFileResult` | Verifies a `.authforge` file with this client's app id / keys / HWID without changing state |
|
|
@@ -295,6 +295,8 @@ Recognized server errors (`KNOWN_SERVER_ERRORS`):
|
|
|
295
295
|
|
|
296
296
|
**`activate_offline()`** does not trigger `on_failure` or `os._exit` either: it raises `AuthForgeError` (see [Self-serve offline activation](#self-serve-offline-activation)).
|
|
297
297
|
|
|
298
|
+
**`self_ban()`** does not trigger `on_failure` or `os._exit` either: every failure raises, a server rejection as `ValueError(code)` and a network failure as `AuthForgeError` with `exc.code` `network_error` or `timeout`.
|
|
299
|
+
|
|
298
300
|
Request retries are automatic inside the internal HTTP layer:
|
|
299
301
|
- `rate_limited`, or HTTP 429 with no error code: retry after 2s, then 5s (max 3 attempts total)
|
|
300
302
|
- `no_credits`, `demo_quota_exceeded` and `app_burn_cap_reached` also arrive as HTTP 429 but are never retried immediately; online check-ins try again at the next interval
|
|
@@ -315,7 +317,7 @@ A failed check-in only counts as an AuthForge verdict when the body is a JSON ob
|
|
|
315
317
|
| Fatal | `revoked`, `expired`, `hwid_mismatch` (the HWID is no longer bound to the license, for example after an HWID reset), `blocked` (HWID/IP blacklisted or not whitelisted), `session_expired` (also raised locally when the grace period or session TTL runs out), `malformed_request`, `app_disabled`, `invalid_app`, `signature_mismatch` (plus the `activate_offline()`-only codes `offline_activation_*` and `offline_file_rejected`) | Clears the stored session (as `logout()` does) and stops background checks **before** calling `on_failure`, so the grace period cannot keep the app running on it. `on_failure` may call `login()` again. |
|
|
316
318
|
| Transient | Everything else: `network_error`, `timeout`, `rate_limited`, `system_error`, `no_credits`, `demo_quota_exceeded`, `app_burn_cap_reached`, `bad_request`, `invalid_key`, every `http_error_<status>`, `invalid_json_response`, `unexpected_response`, `nonce_mismatch`, any unrecognized code | Keeps the session and checks in again on the next `heartbeat_interval`. Once the signed session's TTL has passed, the next transient failure is reported as a fatal `session_expired` instead. |
|
|
317
319
|
|
|
318
|
-
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`.
|
|
320
|
+
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`. Network failures are reported once, with code `network_error` or `timeout`: as `heartbeat_failed` from check-ins and as `login_failed` from `login()`, never as a separate `network_error` reason. `activate_offline()` and `self_ban()` raise them as `AuthForgeError` instead.
|
|
319
321
|
|
|
320
322
|
`on_failure` runs on the background heartbeat thread with no SDK lock held, so calling `logout()`, `is_authenticated()` or `login()` from it is safe. A check-in that is still in flight when you call `logout()` or `login()` is discarded: it cannot restore the old session or report a failure for it.
|
|
321
323
|
|
|
@@ -385,11 +387,12 @@ client.self_ban(
|
|
|
385
387
|
- Uses post-session mode when a session token is available (`session_token` arg or current SDK session).
|
|
386
388
|
- Falls back to pre-session mode using `license_key` + nonce + app secret.
|
|
387
389
|
- In pre-session mode, revoke is forced off client-side to avoid unsafe key revocations.
|
|
390
|
+
- Every failure raises: a server rejection as `ValueError(code)`, a network failure (after one retry) as `AuthForgeError` with `exc.code` `network_error` or `timeout`. `self_ban()` never calls `on_failure` or `os._exit`, with or without a callback.
|
|
388
391
|
- Not available after `login_from_file()`: offline sessions have no server session, so `self_ban()` with no explicit `license_key` / `session_token` raises `ValueError("offline_session")` without contacting the server.
|
|
389
392
|
|
|
390
393
|
## How It Works
|
|
391
394
|
|
|
392
|
-
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it
|
|
395
|
+
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it derives the HWID from the OS machine id (see [Hardware ID](#hardware-id)). It then generates a random nonce and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload with a TTL. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
|
|
393
396
|
|
|
394
397
|
2. **Background checks**: a daemon thread wakes at the configured interval. By default it enforces the grace period: it re-verifies the stored signature and checks expiry without network calls. With `online_heartbeat=True`, it instead sends `/auth/heartbeat` with a fresh nonce and verifies the response.
|
|
395
398
|
|
|
@@ -397,18 +400,29 @@ client.self_ban(
|
|
|
397
400
|
|
|
398
401
|
## Hardware ID
|
|
399
402
|
|
|
400
|
-
The
|
|
401
|
-
|
|
402
|
-
-
|
|
403
|
-
|
|
403
|
+
The HWID (v2, since 2.0) is
|
|
404
|
+
|
|
405
|
+
`SHA256("authforge-hwid-v2|" + app_id + "|" + machine_id)` as lowercase hex,
|
|
406
|
+
|
|
407
|
+
where `machine_id` is the operating system's own machine id, trimmed and lowercased:
|
|
408
|
+
|
|
409
|
+
| OS | Source |
|
|
410
|
+
| -- | ------ |
|
|
411
|
+
| macOS | `IOPlatformUUID` (`/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice`) |
|
|
412
|
+
| Linux | `/etc/machine-id`, then `/var/lib/dbus/machine-id` |
|
|
413
|
+
| Windows | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (64-bit registry view, via `winreg`) |
|
|
414
|
+
|
|
415
|
+
These ids survive network changes, renames, disk usage and USB devices. Including the app id means two different apps on the same machine get unrelated HWIDs. Every AuthForge SDK computes the same value for the same app id and machine; `hwid_vectors.json` pins this. The HWID is sent with every auth request so the server can enforce per-device license limits.
|
|
416
|
+
|
|
417
|
+
If no valid OS id can be read (for example a container without `/etc/machine-id`), the SDK falls back to `legacy:<the pre-2.0 fingerprint of MAC, CPU and disk serial>` as the machine id. Containers built from one image can also share a baked-in machine id, so for containers prefer `hwid_override` with an identifier you control.
|
|
404
418
|
|
|
405
|
-
|
|
419
|
+
The helpers `hwid_from_machine_id`, `normalize_machine_id`, `parse_ioreg_platform_uuid` and `parse_reg_query_machine_guid` are exported for tooling and tests.
|
|
406
420
|
|
|
407
421
|
For non-device identities (for example Telegram users), pass `hwid_override` such as `tg:<user_id>`.
|
|
408
422
|
|
|
409
423
|
## Test Vectors
|
|
410
424
|
|
|
411
|
-
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code.
|
|
425
|
+
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code. `hwid_vectors.json` (generated by `authforge-node/generate_hwid_vectors.mjs`) is the cross-SDK suite for HWID v2: derivation, machine id normalization, and the `ioreg` / `reg query` parsers.
|
|
412
426
|
|
|
413
427
|
## Requirements
|
|
414
428
|
|
|
@@ -88,7 +88,7 @@ _ACTIVATION_REQUEST_VERSION = 1
|
|
|
88
88
|
_ACTIVATION_REQUEST_TYP = "authforge-activation-request"
|
|
89
89
|
_BEGIN_ACTIVATION_REQUEST = "-----BEGIN AUTHFORGE ACTIVATION REQUEST-----"
|
|
90
90
|
_END_ACTIVATION_REQUEST = "-----END AUTHFORGE ACTIVATION REQUEST-----"
|
|
91
|
-
_SDK_TAG = "python/
|
|
91
|
+
_SDK_TAG = "python/2.0.0"
|
|
92
92
|
_ARMOR_LINE_WIDTH = 64
|
|
93
93
|
_MAX_REQUEST_HWID = 256
|
|
94
94
|
_MAX_REQUEST_MACHINE_NAME = 128
|
|
@@ -621,6 +621,100 @@ def is_transient_error(error: Union[str, BaseException, None]) -> bool:
|
|
|
621
621
|
return code not in DEFINITIVE_ERROR_CODES
|
|
622
622
|
|
|
623
623
|
|
|
624
|
+
# HWID v2: sha256("authforge-hwid-v2|" + app_id + "|" + machine_id), where machine_id is
|
|
625
|
+
# the OS machine id (macOS IOPlatformUUID, Linux /etc/machine-id, Windows MachineGuid).
|
|
626
|
+
# Every SDK derives the same value; conformance is pinned by hwid_vectors.json.
|
|
627
|
+
HWID_V2_PREFIX = "authforge-hwid-v2|"
|
|
628
|
+
_MACHINE_ID_COMMAND_TIMEOUT = 2
|
|
629
|
+
_LINUX_MACHINE_ID_PATHS = ("/etc/machine-id", "/var/lib/dbus/machine-id")
|
|
630
|
+
_MACHINE_ID_RE = re.compile(r"[0-9a-f-]+")
|
|
631
|
+
_ALL_ZERO_MACHINE_ID_RE = re.compile(r"[0-]+")
|
|
632
|
+
_IOREG_PLATFORM_UUID_RE = re.compile(r'"IOPlatformUUID"\s*=\s*"([^"]+)"')
|
|
633
|
+
_ASCII_WS_SPLIT_RE = re.compile(r"[ \t\r\n]+")
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def hwid_from_machine_id(app_id: str, machine_id: str) -> str:
|
|
637
|
+
"""Derive the HWID from an already-normalized machine id (or a ``legacy:`` fallback id)."""
|
|
638
|
+
material = f"{HWID_V2_PREFIX}{app_id}|{machine_id}"
|
|
639
|
+
return hashlib.sha256(material.encode("utf-8")).hexdigest()
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def normalize_machine_id(raw: Optional[str]) -> Optional[str]:
|
|
643
|
+
"""Trim and lowercase an OS machine id; ``None`` when it is not a usable id."""
|
|
644
|
+
if not isinstance(raw, str):
|
|
645
|
+
return None
|
|
646
|
+
value = raw.strip(" \t\r\n")
|
|
647
|
+
# ASCII-only lowercasing; the character check below rejects anything non-ASCII.
|
|
648
|
+
value = "".join(chr(ord(c) + 32) if "A" <= c <= "Z" else c for c in value)
|
|
649
|
+
if len(value) < 8 or len(value) > 128:
|
|
650
|
+
return None
|
|
651
|
+
if not _MACHINE_ID_RE.fullmatch(value):
|
|
652
|
+
return None
|
|
653
|
+
if _ALL_ZERO_MACHINE_ID_RE.fullmatch(value):
|
|
654
|
+
return None
|
|
655
|
+
return value
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def parse_ioreg_platform_uuid(output: Optional[str]) -> Optional[str]:
|
|
659
|
+
"""Extract IOPlatformUUID from ``ioreg -rd1 -c IOPlatformExpertDevice`` output."""
|
|
660
|
+
if not isinstance(output, str):
|
|
661
|
+
return None
|
|
662
|
+
match = _IOREG_PLATFORM_UUID_RE.search(output)
|
|
663
|
+
return match.group(1) if match else None
|
|
664
|
+
|
|
665
|
+
|
|
666
|
+
def parse_reg_query_machine_guid(output: Optional[str]) -> Optional[str]:
|
|
667
|
+
"""Extract MachineGuid from ``reg.exe query ... /v MachineGuid`` output."""
|
|
668
|
+
if not isinstance(output, str):
|
|
669
|
+
return None
|
|
670
|
+
for line in re.split(r"\r?\n", output):
|
|
671
|
+
tokens = [t for t in _ASCII_WS_SPLIT_RE.split(line) if t]
|
|
672
|
+
if len(tokens) >= 3 and tokens[0].lower() == "machineguid" and tokens[1] == "REG_SZ":
|
|
673
|
+
return tokens[2]
|
|
674
|
+
return None
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def _read_os_machine_id() -> Optional[str]:
|
|
678
|
+
"""The normalized OS machine id, or ``None``. Never raises."""
|
|
679
|
+
try:
|
|
680
|
+
if sys.platform == "darwin":
|
|
681
|
+
result = subprocess.run(
|
|
682
|
+
["/usr/sbin/ioreg", "-rd1", "-c", "IOPlatformExpertDevice"],
|
|
683
|
+
capture_output=True,
|
|
684
|
+
text=True,
|
|
685
|
+
timeout=_MACHINE_ID_COMMAND_TIMEOUT,
|
|
686
|
+
)
|
|
687
|
+
if result.returncode != 0:
|
|
688
|
+
return None
|
|
689
|
+
return normalize_machine_id(parse_ioreg_platform_uuid(result.stdout))
|
|
690
|
+
if sys.platform.startswith("linux"):
|
|
691
|
+
for path in _LINUX_MACHINE_ID_PATHS:
|
|
692
|
+
try:
|
|
693
|
+
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
694
|
+
machine_id = normalize_machine_id(handle.read())
|
|
695
|
+
except Exception:
|
|
696
|
+
continue # missing or unreadable: try the next source
|
|
697
|
+
if machine_id:
|
|
698
|
+
return machine_id
|
|
699
|
+
return None
|
|
700
|
+
if sys.platform == "win32":
|
|
701
|
+
import winreg
|
|
702
|
+
|
|
703
|
+
with winreg.OpenKey(
|
|
704
|
+
winreg.HKEY_LOCAL_MACHINE,
|
|
705
|
+
r"SOFTWARE\Microsoft\Cryptography",
|
|
706
|
+
0,
|
|
707
|
+
winreg.KEY_READ | winreg.KEY_WOW64_64KEY,
|
|
708
|
+
) as key:
|
|
709
|
+
value, value_type = winreg.QueryValueEx(key, "MachineGuid")
|
|
710
|
+
if value_type != winreg.REG_SZ or not isinstance(value, str):
|
|
711
|
+
return None
|
|
712
|
+
return normalize_machine_id(value)
|
|
713
|
+
except Exception:
|
|
714
|
+
pass # fall through to the legacy fingerprint
|
|
715
|
+
return None
|
|
716
|
+
|
|
717
|
+
|
|
624
718
|
def _warn(message: str) -> None:
|
|
625
719
|
# sys.stderr is None under pythonw, and a closed stream must not stop check-ins.
|
|
626
720
|
stream = sys.stderr
|
|
@@ -848,9 +942,8 @@ class AuthForgeClient:
|
|
|
848
942
|
"nonce": self._generate_nonce(),
|
|
849
943
|
}
|
|
850
944
|
try:
|
|
851
|
-
#
|
|
852
|
-
|
|
853
|
-
response_obj = self._post_json("/auth/offline/activate", body, skip_failure_hook=True)
|
|
945
|
+
# A network failure must raise here, never reach on_failure / os._exit.
|
|
946
|
+
response_obj = self._post_json("/auth/offline/activate", body)
|
|
854
947
|
except AuthForgeError:
|
|
855
948
|
raise
|
|
856
949
|
except Exception as exc:
|
|
@@ -990,7 +1083,7 @@ class AuthForgeClient:
|
|
|
990
1083
|
}
|
|
991
1084
|
if self.ttl_seconds is not None:
|
|
992
1085
|
body["ttlSeconds"] = self.ttl_seconds
|
|
993
|
-
response_obj = self._post_json("/auth/validate", body
|
|
1086
|
+
response_obj = self._post_json("/auth/validate", body)
|
|
994
1087
|
expected_nonce = str(body.get("nonce", "")).strip()
|
|
995
1088
|
parsed = self._parse_validate_success(response_obj, expected_nonce)
|
|
996
1089
|
result: ValidateLicenseSuccess = {
|
|
@@ -1049,6 +1142,8 @@ class AuthForgeClient:
|
|
|
1049
1142
|
|
|
1050
1143
|
resolved_session = resolved_session or current_session
|
|
1051
1144
|
|
|
1145
|
+
# Every failure raises, a network failure as AuthForgeError
|
|
1146
|
+
# network_error / timeout; self_ban never calls on_failure / os._exit.
|
|
1052
1147
|
if resolved_session:
|
|
1053
1148
|
body: Dict[str, Any] = {
|
|
1054
1149
|
"appId": self.app_id,
|
|
@@ -1181,7 +1276,7 @@ class AuthForgeClient:
|
|
|
1181
1276
|
"hwid": hwid,
|
|
1182
1277
|
}
|
|
1183
1278
|
# Network failures surface once, as heartbeat_failed / network_error.
|
|
1184
|
-
response_obj = self._post_json("/auth/heartbeat", body
|
|
1279
|
+
response_obj = self._post_json("/auth/heartbeat", body)
|
|
1185
1280
|
if not self._is_success_status(response_obj.get("status")):
|
|
1186
1281
|
raise self._heartbeat_verdict(response_obj)
|
|
1187
1282
|
expected_nonce = str(body.get("nonce", "")).strip()
|
|
@@ -1240,6 +1335,7 @@ class AuthForgeClient:
|
|
|
1240
1335
|
}
|
|
1241
1336
|
if self.ttl_seconds is not None:
|
|
1242
1337
|
body["ttlSeconds"] = self.ttl_seconds
|
|
1338
|
+
# Network failures surface once, as login_failed / network_error.
|
|
1243
1339
|
response_obj = self._post_json("/auth/validate", body)
|
|
1244
1340
|
expected_nonce = str(body.get("nonce", "")).strip()
|
|
1245
1341
|
self._apply_signed_response(
|
|
@@ -1344,9 +1440,7 @@ class AuthForgeClient:
|
|
|
1344
1440
|
self._license_variables = parsed["license_variables"]
|
|
1345
1441
|
self._authenticated = True
|
|
1346
1442
|
|
|
1347
|
-
def _post_json(
|
|
1348
|
-
self, path: str, data: Dict[str, Any], *, skip_failure_hook: bool = False
|
|
1349
|
-
) -> Dict[str, Any]:
|
|
1443
|
+
def _post_json(self, path: str, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1350
1444
|
url = f"{self.api_base_url}{path}"
|
|
1351
1445
|
body = dict(data)
|
|
1352
1446
|
rate_attempt = 0
|
|
@@ -1386,8 +1480,6 @@ class AuthForgeClient:
|
|
|
1386
1480
|
network_attempt += 1
|
|
1387
1481
|
time.sleep(NETWORK_RETRY_DELAY)
|
|
1388
1482
|
continue
|
|
1389
|
-
if not skip_failure_hook:
|
|
1390
|
-
self._fail("network_error", exc)
|
|
1391
1483
|
reason = getattr(exc, "reason", exc)
|
|
1392
1484
|
code = "timeout" if isinstance(reason, (socket.timeout, TimeoutError)) else "network_error"
|
|
1393
1485
|
raise AuthForgeError(code, f"url_error: {exc}") from exc
|
|
@@ -1414,6 +1506,11 @@ class AuthForgeClient:
|
|
|
1414
1506
|
return obj
|
|
1415
1507
|
|
|
1416
1508
|
def _get_hwid(self) -> str:
|
|
1509
|
+
machine_id = _read_os_machine_id() or "legacy:" + self._legacy_fingerprint()
|
|
1510
|
+
return hwid_from_machine_id(self.app_id, machine_id)
|
|
1511
|
+
|
|
1512
|
+
# Pre-2.0 fingerprint, kept only as the fallback when no OS machine id is readable.
|
|
1513
|
+
def _legacy_fingerprint(self) -> str:
|
|
1417
1514
|
mac = self._safe_mac_address()
|
|
1418
1515
|
cpu = self._safe_cpu_info()
|
|
1419
1516
|
disk = self._safe_disk_serial()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
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
|
|
@@ -52,7 +52,7 @@ Everything in this list ships in `authforge.py` today:
|
|
|
52
52
|
- **Offline license files (`.authforge`)**: `login_from_file()` / `verify_license_file()` verify a cloud-minted, Ed25519-signed file with zero network access for air-gapped machines.
|
|
53
53
|
- **Self-serve offline activation**: `activate_offline()` turns a license key into a lifetime, HWID-bound `.authforge` file with one online call, then runs offline from that file.
|
|
54
54
|
- **Nonce anti-replay**: a fresh 128-bit nonce is sent on every request and the echoed nonce in the signed payload is checked before the response is accepted.
|
|
55
|
-
- **HWID
|
|
55
|
+
- **HWID v2**: per-app device id derived from the OS machine id (macOS `IOPlatformUUID`, Linux `/etc/machine-id`, Windows `MachineGuid`), identical across all AuthForge SDKs for the same app and machine.
|
|
56
56
|
- **`hwid_override`**: bind to any identity instead of the machine (for example `tg:<id>`, `discord:<id>`).
|
|
57
57
|
- **Seat enforcement**: the server binds each HWID into a license's free slots up to `maxHwidSlots`; `hwid_count` / `max_hwid_slots` are surfaced on the result. A shared (unlimited-seat) key skips per-device binding.
|
|
58
58
|
- **Self-ban** (`self_ban()`) for anti-tamper response, both pre-session and post-session.
|
|
@@ -119,7 +119,7 @@ client = AuthForgeClient(
|
|
|
119
119
|
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
|
|
120
120
|
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
|
|
121
121
|
| `ttl_seconds` | `int \| None` | `None` (server default: 86400) | The grace period duration: how long the app keeps running on the signed session without contacting AuthForge. Server clamps to `[3600, 604800]` (1h to 7d); preserved across check-in refreshes. |
|
|
122
|
-
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of machine
|
|
122
|
+
| `hwid_override` | `str \| None` | `None` | Optional custom hardware/subject identifier. When set to a non-empty value, the SDK uses it instead of deriving the HWID from the OS machine id. |
|
|
123
123
|
| `online_heartbeat` | bool (keyword-only) | `False` | Enable online check-ins: periodic `POST /auth/heartbeat` for fast revocation and concurrent-use detection. |
|
|
124
124
|
|
|
125
125
|
### Identity-based binding example (Telegram/Discord)
|
|
@@ -190,7 +190,7 @@ if client.login_from_file("license.authforge"):
|
|
|
190
190
|
print(client.get_license_variables())
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
Since 2.0 every AuthForge SDK computes the same HWID for the same app id and machine, but HWIDs collected before upgrading to 2.0 no longer match: collect the HWID again after upgrading. After `login_from_file()`, `get_session_kind()` returns `"offline"` (`"online"` after `login()`, `None` when logged out).
|
|
194
194
|
|
|
195
195
|
`verify_license_file()` (module function and client method) performs the same checks without touching client state. Failure codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `login_from_file()` reports them through `on_failure("offline_login_failed", exc)` and returns `False`; it never calls `os._exit`.
|
|
196
196
|
|
|
@@ -297,7 +297,7 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
297
297
|
|---|---|---|
|
|
298
298
|
| `login(license_key)` | `bool` | Activates: validates the key online and stores the signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
|
|
299
299
|
| `validate_license(license_key)` | `ValidateLicenseResult` | Same `/auth/validate` + signatures as `login`; does not store session or start background checks; returns a dict with `valid` / `code` and **never** calls `on_failure` or `os._exit` |
|
|
300
|
-
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only) |
|
|
300
|
+
| `self_ban(...)` | `dict` | Requests `/auth/selfban` to blacklist HWID/IP and optionally revoke (session-authenticated only). Raises `ValueError` on any failure (a network failure as `AuthForgeError` `network_error` / `timeout`); never calls `on_failure` or `os._exit` |
|
|
301
301
|
| `login_from_file(path_or_text)` | `bool` | Authorizes from an offline `.authforge` file with no network; never starts background checks; failures go to `on_failure("offline_login_failed", …)` |
|
|
302
302
|
| `activate_offline(license_key, path=None)` | `bool` | Self-serve offline activation: one online call returns a lifetime HWID-bound `.authforge` file, verified locally, written atomically to `path` and applied like `login_from_file`. Needs the App Secret; raises `AuthForgeError` on refusal; never calls `on_failure` or `os._exit` |
|
|
303
303
|
| `verify_license_file(path_or_text, *, now=None)` | `VerifyLicenseFileResult` | Verifies a `.authforge` file with this client's app id / keys / HWID without changing state |
|
|
@@ -323,6 +323,8 @@ Recognized server errors (`KNOWN_SERVER_ERRORS`):
|
|
|
323
323
|
|
|
324
324
|
**`activate_offline()`** does not trigger `on_failure` or `os._exit` either: it raises `AuthForgeError` (see [Self-serve offline activation](#self-serve-offline-activation)).
|
|
325
325
|
|
|
326
|
+
**`self_ban()`** does not trigger `on_failure` or `os._exit` either: every failure raises, a server rejection as `ValueError(code)` and a network failure as `AuthForgeError` with `exc.code` `network_error` or `timeout`.
|
|
327
|
+
|
|
326
328
|
Request retries are automatic inside the internal HTTP layer:
|
|
327
329
|
- `rate_limited`, or HTTP 429 with no error code: retry after 2s, then 5s (max 3 attempts total)
|
|
328
330
|
- `no_credits`, `demo_quota_exceeded` and `app_burn_cap_reached` also arrive as HTTP 429 but are never retried immediately; online check-ins try again at the next interval
|
|
@@ -343,7 +345,7 @@ A failed check-in only counts as an AuthForge verdict when the body is a JSON ob
|
|
|
343
345
|
| Fatal | `revoked`, `expired`, `hwid_mismatch` (the HWID is no longer bound to the license, for example after an HWID reset), `blocked` (HWID/IP blacklisted or not whitelisted), `session_expired` (also raised locally when the grace period or session TTL runs out), `malformed_request`, `app_disabled`, `invalid_app`, `signature_mismatch` (plus the `activate_offline()`-only codes `offline_activation_*` and `offline_file_rejected`) | Clears the stored session (as `logout()` does) and stops background checks **before** calling `on_failure`, so the grace period cannot keep the app running on it. `on_failure` may call `login()` again. |
|
|
344
346
|
| Transient | Everything else: `network_error`, `timeout`, `rate_limited`, `system_error`, `no_credits`, `demo_quota_exceeded`, `app_burn_cap_reached`, `bad_request`, `invalid_key`, every `http_error_<status>`, `invalid_json_response`, `unexpected_response`, `nonce_mismatch`, any unrecognized code | Keeps the session and checks in again on the next `heartbeat_interval`. Once the signed session's TTL has passed, the next transient failure is reported as a fatal `session_expired` instead. |
|
|
345
347
|
|
|
346
|
-
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`.
|
|
348
|
+
Transient failures only keep checking in if `on_failure` returns normally. Without a callback, a transient failure prints `AuthForge: background check failed (<code>); retrying next interval` to stderr and check-ins continue; a fatal one, including the `session_expired` a transient failure becomes once the session TTL has passed, still ends the process with `os._exit(1)`. Network failures are reported once, with code `network_error` or `timeout`: as `heartbeat_failed` from check-ins and as `login_failed` from `login()`, never as a separate `network_error` reason. `activate_offline()` and `self_ban()` raise them as `AuthForgeError` instead.
|
|
347
349
|
|
|
348
350
|
`on_failure` runs on the background heartbeat thread with no SDK lock held, so calling `logout()`, `is_authenticated()` or `login()` from it is safe. A check-in that is still in flight when you call `logout()` or `login()` is discarded: it cannot restore the old session or report a failure for it.
|
|
349
351
|
|
|
@@ -413,11 +415,12 @@ client.self_ban(
|
|
|
413
415
|
- Uses post-session mode when a session token is available (`session_token` arg or current SDK session).
|
|
414
416
|
- Falls back to pre-session mode using `license_key` + nonce + app secret.
|
|
415
417
|
- In pre-session mode, revoke is forced off client-side to avoid unsafe key revocations.
|
|
418
|
+
- Every failure raises: a server rejection as `ValueError(code)`, a network failure (after one retry) as `AuthForgeError` with `exc.code` `network_error` or `timeout`. `self_ban()` never calls `on_failure` or `os._exit`, with or without a callback.
|
|
416
419
|
- Not available after `login_from_file()`: offline sessions have no server session, so `self_ban()` with no explicit `license_key` / `session_token` raises `ValueError("offline_session")` without contacting the server.
|
|
417
420
|
|
|
418
421
|
## How It Works
|
|
419
422
|
|
|
420
|
-
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it
|
|
423
|
+
1. **Activate**: `login()` uses `hwid_override` if provided; otherwise it derives the HWID from the OS machine id (see [Hardware ID](#hardware-id)). It then generates a random nonce and sends everything to the AuthForge API. The server validates the license key, binds the HWID, deducts a credit, and returns a signed payload with a TTL. The SDK verifies the Ed25519 signature and nonce to prevent replay attacks.
|
|
421
424
|
|
|
422
425
|
2. **Background checks**: a daemon thread wakes at the configured interval. By default it enforces the grace period: it re-verifies the stored signature and checks expiry without network calls. With `online_heartbeat=True`, it instead sends `/auth/heartbeat` with a fresh nonce and verifies the response.
|
|
423
426
|
|
|
@@ -425,18 +428,29 @@ client.self_ban(
|
|
|
425
428
|
|
|
426
429
|
## Hardware ID
|
|
427
430
|
|
|
428
|
-
The
|
|
429
|
-
|
|
430
|
-
-
|
|
431
|
-
|
|
431
|
+
The HWID (v2, since 2.0) is
|
|
432
|
+
|
|
433
|
+
`SHA256("authforge-hwid-v2|" + app_id + "|" + machine_id)` as lowercase hex,
|
|
434
|
+
|
|
435
|
+
where `machine_id` is the operating system's own machine id, trimmed and lowercased:
|
|
436
|
+
|
|
437
|
+
| OS | Source |
|
|
438
|
+
| -- | ------ |
|
|
439
|
+
| macOS | `IOPlatformUUID` (`/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice`) |
|
|
440
|
+
| Linux | `/etc/machine-id`, then `/var/lib/dbus/machine-id` |
|
|
441
|
+
| Windows | `HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid` (64-bit registry view, via `winreg`) |
|
|
442
|
+
|
|
443
|
+
These ids survive network changes, renames, disk usage and USB devices. Including the app id means two different apps on the same machine get unrelated HWIDs. Every AuthForge SDK computes the same value for the same app id and machine; `hwid_vectors.json` pins this. The HWID is sent with every auth request so the server can enforce per-device license limits.
|
|
444
|
+
|
|
445
|
+
If no valid OS id can be read (for example a container without `/etc/machine-id`), the SDK falls back to `legacy:<the pre-2.0 fingerprint of MAC, CPU and disk serial>` as the machine id. Containers built from one image can also share a baked-in machine id, so for containers prefer `hwid_override` with an identifier you control.
|
|
432
446
|
|
|
433
|
-
|
|
447
|
+
The helpers `hwid_from_machine_id`, `normalize_machine_id`, `parse_ioreg_platform_uuid` and `parse_reg_query_machine_guid` are exported for tooling and tests.
|
|
434
448
|
|
|
435
449
|
For non-device identities (for example Telegram users), pass `hwid_override` such as `tg:<user_id>`.
|
|
436
450
|
|
|
437
451
|
## Test Vectors
|
|
438
452
|
|
|
439
|
-
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code.
|
|
453
|
+
The shared `test_vectors.json` file validates cross-language Ed25519 verification behavior. `offline_license_vectors.json` (generated from a fixed test seed in the Node SDK repo) is the cross-SDK conformance suite for `.authforge` offline license files: good files plus the `bad_signature`, wrong key, `wrong_app`, `expired`, `hwid_mismatch`, `unsupported_version` and `bad_armor` rejects. `self_serve_activation_vectors.json` is the cross-SDK suite for `activate_offline()`: mocked `/auth/offline/activate` responses with the expected written file, session and error code. `hwid_vectors.json` (generated by `authforge-node/generate_hwid_vectors.mjs`) is the cross-SDK suite for HWID v2: derivation, machine id normalization, and the `ioreg` / `reg query` parsers.
|
|
440
454
|
|
|
441
455
|
## Requirements
|
|
442
456
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "authforge-sdk"
|
|
7
|
-
version = "
|
|
7
|
+
version = "2.0.0"
|
|
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
|