Skip to content

Commit 418ff61

Browse files
committed
Retry && Agents
1 parent 3d07173 commit 418ff61

3 files changed

Lines changed: 273 additions & 40 deletions

File tree

AGENTS.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# AuthForge SDK — AI Agent Reference
2+
3+
> This file is optimized for AI coding agents (Cursor, Copilot, Claude Code, etc.).
4+
> It contains everything needed to correctly integrate AuthForge licensing into a project.
5+
6+
## What AuthForge does
7+
8+
AuthForge is a license key validation service. Your app sends a license key + hardware ID to the AuthForge API, gets back a cryptographically signed response, and runs background heartbeats to maintain the session. If the license is revoked or expired, the heartbeat fails and you handle it (typically exit the app).
9+
10+
## Installation
11+
12+
Copy `authforge.py` into your project (single file, stdlib only). Requires Python 3.9+.
13+
14+
## Minimal working integration
15+
16+
```python
17+
import sys
18+
from typing import Optional
19+
20+
from authforge import AuthForgeClient
21+
22+
23+
def on_failure(reason: str, exc: Optional[Exception]) -> None:
24+
print(f"AuthForge: {reason}", file=sys.stderr)
25+
if exc is not None:
26+
print(exc, file=sys.stderr)
27+
sys.exit(1)
28+
29+
30+
def main() -> None:
31+
client = AuthForgeClient(
32+
app_id="YOUR_APP_ID",
33+
app_secret="YOUR_APP_SECRET",
34+
heartbeat_mode="SERVER",
35+
on_failure=on_failure,
36+
)
37+
license_key = input("Enter license key: ").strip()
38+
if not client.login(license_key):
39+
print("Login failed.", file=sys.stderr)
40+
sys.exit(1)
41+
# --- Your application code starts here ---
42+
print("Running with a valid license.")
43+
# --- Your application code ends here ---
44+
client.logout()
45+
46+
47+
if __name__ == "__main__":
48+
main()
49+
```
50+
51+
## Constructor parameters
52+
53+
| Parameter | Type | Required | Default | Description |
54+
|-----------|------|----------|---------|-------------|
55+
| `app_id` | `str` | yes || Application ID |
56+
| `app_secret` | `str` | yes || Application secret |
57+
| `heartbeat_mode` | `str` | yes || `"SERVER"` or `"LOCAL"` (case-insensitive) |
58+
| `heartbeat_interval` | `int` | no | `900` | Seconds between heartbeats |
59+
| `api_base_url` | `str` | no | `https://auth.authforge.cc` | API base URL |
60+
| `on_failure` | `Callable[[str, Optional[Exception]], None] \| None` | no | `None` | Called on login/heartbeat/network failure; if omitted, process exits via `os._exit(1)` |
61+
| `request_timeout` | `int` | no | `15` | HTTP timeout (seconds) |
62+
63+
## Methods
64+
65+
| Method | Returns | Description |
66+
|--------|---------|-------------|
67+
| `login(license_key: str)` | `bool` | Validates license, verifies signatures, starts heartbeat thread |
68+
| `logout()` | `None` | Stops heartbeat and clears session state |
69+
| `is_authenticated()` | `bool` | Whether a session token is present and marked authenticated |
70+
| `get_session_data()` | `dict \| None` | Decoded signed payload map |
71+
| `get_app_variables()` | `dict \| None` | App-scoped variables |
72+
| `get_license_variables()` | `dict \| None` | License-scoped variables |
73+
74+
## Error codes the server can return
75+
76+
invalid_app, invalid_key, expired, revoked, hwid_mismatch, no_credits, blocked, rate_limited, replay_detected, checksum_required, checksum_mismatch, session_expired, app_disabled
77+
78+
## Common patterns
79+
80+
### Reading license variables (feature gating)
81+
82+
```python
83+
vars_map = client.get_license_variables() or {}
84+
tier = vars_map.get("tier")
85+
```
86+
87+
### Graceful shutdown
88+
89+
```python
90+
client.logout()
91+
```
92+
93+
### Custom error handling
94+
95+
Server error codes appear as `ValueError` in the `exc` passed to `on_failure` from failed validation (e.g. `invalid_key`). Reasons are `login_failed`, `heartbeat_failed`, or `network_error`.
96+
97+
```python
98+
import sys
99+
from typing import Optional
100+
101+
def on_failure(reason: str, exc: Optional[Exception]) -> None:
102+
if isinstance(exc, ValueError) and exc.args:
103+
code = str(exc.args[0])
104+
if code in {"invalid_key", "expired", "revoked"}:
105+
print(f"License issue: {code}", file=sys.stderr)
106+
sys.exit(1)
107+
```
108+
109+
## Do NOT
110+
111+
- Do not hardcode the app secret as a plain string literal in source — use environment variables or encrypted config
112+
- Do not skip the `on_failure` callback — without it, heartbeat failures terminate the process via `os._exit(1)` without your cleanup
113+
- Do not call `login()` on every app action — call it once at startup; heartbeats handle the rest
114+
- Do not use `heartbeat_mode="LOCAL"` unless the app has no internet after initial auth

README.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,35 @@ else:
3939
| `on_failure` | callable | `None` | Callback `(reason: str, exc: Exception | None)` on auth failure |
4040
| `request_timeout` | int | `15` | HTTP request timeout in seconds |
4141

42+
## Methods
43+
44+
| Method | Returns | Description |
45+
|---|---|---|
46+
| `login(license_key)` | `bool` | Validates key and stores signed session (`sessionToken`, `expiresIn`, `appVariables`, `licenseVariables`) |
47+
| `logout()` | `None` | Stops heartbeat and clears all session/auth state |
48+
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
49+
| `get_session_data()` | `dict \| None` | Full decoded payload map |
50+
| `get_app_variables()` | `dict \| None` | App-scoped variables map |
51+
| `get_license_variables()` | `dict \| None` | License-scoped variables map |
52+
4253
## Heartbeat Modes
4354

44-
**SERVER** — The SDK pings the AuthForge API every `heartbeat_interval` seconds with a fresh nonce. Each response is cryptographically verified. If the license is revoked or the session expires, the failure handler triggers.
55+
**SERVER** — The SDK calls `/auth/heartbeat` every `heartbeat_interval` seconds with a fresh nonce, verifies signature + nonce, and triggers failure on invalid session state.
4556

46-
**LOCAL** — No network requests during heartbeats. The SDK verifies the stored HMAC signature and checks that the session hasn't expired. When the prepaid block runs out, it makes a single network call to refresh. Use this for apps where you want minimal network overhead.
57+
**LOCAL** — No network calls. The SDK re-verifies stored signature state and checks expiry timestamp locally. If expired, it triggers failure with `session_expired`.
4758

4859
## Failure Handling
4960

5061
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.
5162

63+
Recognized server errors:
64+
`invalid_app`, `invalid_key`, `expired`, `revoked`, `hwid_mismatch`, `no_credits`, `blocked`, `rate_limited`, `replay_detected`, `app_disabled`, `session_expired`, `bad_request`, `checksum_required`, `checksum_mismatch`
65+
66+
Request retries are automatic inside the internal HTTP layer:
67+
- `rate_limited`: retry after 2s, then 5s (max 3 attempts total)
68+
- network failure: retry once after 2s
69+
- every retry regenerates a fresh nonce
70+
5271
```python
5372
def handle_auth_failure(reason, exception):
5473
print(f"Auth failed: {reason}")

0 commit comments

Comments
 (0)