authforge-sdk 1.2.0__tar.gz → 1.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.
- {authforge_sdk-1.2.0/authforge_sdk.egg-info → authforge_sdk-1.3.0}/PKG-INFO +8 -6
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/README.md +7 -5
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/authforge.py +199 -3
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0/authforge_sdk.egg-info}/PKG-INFO +8 -6
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/pyproject.toml +1 -1
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/LICENSE +0 -0
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/authforge_sdk.egg-info/SOURCES.txt +0 -0
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/authforge_sdk.egg-info/dependency_links.txt +0 -0
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/authforge_sdk.egg-info/requires.txt +0 -0
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/authforge_sdk.egg-info/top_level.txt +0 -0
- {authforge_sdk-1.2.0 → authforge_sdk-1.3.0}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -110,7 +110,7 @@ client = AuthForgeClient(
|
|
|
110
110
|
| Parameter | Type | Default | Description |
|
|
111
111
|
|---|---|---|---|
|
|
112
112
|
| `app_id` | str | required | Your application ID from the AuthForge dashboard |
|
|
113
|
-
| `app_secret` | str | required | Your application secret from the AuthForge dashboard |
|
|
113
|
+
| `app_secret` | str | required for online APIs; `None` / `""` for `login_from_file` only | Your application secret from the AuthForge dashboard. Do not ship it in air-gapped binaries. |
|
|
114
114
|
| `public_key` | `str \| Sequence[str]` | required | App Ed25519 public key(s) (base64) from dashboard. Pass one key, a list of keys, or a comma-separated string to trust multiple keys during rotation (see [Key rotation](#key-rotation)). |
|
|
115
115
|
| `heartbeat_mode` | `str \| None` | `None` | Deprecated shim (see [Migrating from heartbeat_mode](#migrating-from-heartbeat_mode)). Use `online_heartbeat` instead. |
|
|
116
116
|
| `heartbeat_interval` | int | `900` | Seconds between background checks (minimum `10`; default 15 min). Applies to both grace period checks and online check-ins. |
|
|
@@ -159,7 +159,7 @@ Tune `ttl_seconds` to set the grace period duration (server default 24h, clamped
|
|
|
159
159
|
|
|
160
160
|
## Offline license files (`.authforge`)
|
|
161
161
|
|
|
162
|
-
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins.
|
|
162
|
+
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins. Pass `app_secret=None` (or `""`) so the air-gapped binary does not contain the App Secret.
|
|
163
163
|
|
|
164
164
|
| | Grace period (default) | Offline license file |
|
|
165
165
|
| --- | --- | --- |
|
|
@@ -174,13 +174,13 @@ from authforge import AuthForgeClient
|
|
|
174
174
|
|
|
175
175
|
client = AuthForgeClient(
|
|
176
176
|
app_id="YOUR_APP_ID",
|
|
177
|
-
app_secret=
|
|
177
|
+
app_secret=None, # login_from_file does not use the App Secret; do not ship it in air-gapped builds
|
|
178
178
|
public_key="YOUR_PUBLIC_KEY",
|
|
179
179
|
on_failure=lambda reason, exc: print(reason, exc),
|
|
180
180
|
)
|
|
181
181
|
|
|
182
|
-
# 1.
|
|
183
|
-
|
|
182
|
+
# 1. Write an activation request the operator drops into the mint dialog:
|
|
183
|
+
client.write_activation_request("machine.authforge-request")
|
|
184
184
|
|
|
185
185
|
# 2. Later, authorize from the minted file (path or armored text). No network.
|
|
186
186
|
if client.login_from_file("license.authforge"):
|
|
@@ -232,6 +232,8 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
232
232
|
| `get_offline_license()` | `dict \| None` | Metadata of the offline file in use (`jti`, `expires_at`, `hwid_policy`, …) |
|
|
233
233
|
| `get_session_kind()` | `"online" \| "offline" \| None` | Which kind of session the client holds (`None` when logged out) |
|
|
234
234
|
| `get_hwid()` | `str` | The HWID this client sends (or `hwid_override`); customers share it to receive a bound file |
|
|
235
|
+
| `create_activation_request(**kwargs)` | `str` | Unsigned `.authforge-request` for this machine. No network, no secret. Hostname omitted unless `include_machine_name=True` |
|
|
236
|
+
| `write_activation_request(path, **kwargs)` | `None` | Writes that file as UTF-8 |
|
|
235
237
|
| `logout()` | `None` | Stops background checks and clears all session/auth state |
|
|
236
238
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
237
239
|
| `get_session_data()` | `dict \| None` | Full decoded payload map |
|
|
@@ -82,7 +82,7 @@ client = AuthForgeClient(
|
|
|
82
82
|
| Parameter | Type | Default | Description |
|
|
83
83
|
|---|---|---|---|
|
|
84
84
|
| `app_id` | str | required | Your application ID from the AuthForge dashboard |
|
|
85
|
-
| `app_secret` | str | required | Your application secret from the AuthForge dashboard |
|
|
85
|
+
| `app_secret` | str | required for online APIs; `None` / `""` for `login_from_file` only | Your application secret from the AuthForge dashboard. Do not ship it in air-gapped binaries. |
|
|
86
86
|
| `public_key` | `str \| Sequence[str]` | required | App Ed25519 public key(s) (base64) from dashboard. Pass one key, a list of keys, or a comma-separated string to trust multiple keys during rotation (see [Key rotation](#key-rotation)). |
|
|
87
87
|
| `heartbeat_mode` | `str \| None` | `None` | Deprecated shim (see [Migrating from heartbeat_mode](#migrating-from-heartbeat_mode)). Use `online_heartbeat` instead. |
|
|
88
88
|
| `heartbeat_interval` | int | `900` | Seconds between background checks (minimum `10`; default 15 min). Applies to both grace period checks and online check-ins. |
|
|
@@ -131,7 +131,7 @@ Tune `ttl_seconds` to set the grace period duration (server default 24h, clamped
|
|
|
131
131
|
|
|
132
132
|
## Offline license files (`.authforge`)
|
|
133
133
|
|
|
134
|
-
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins.
|
|
134
|
+
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins. Pass `app_secret=None` (or `""`) so the air-gapped binary does not contain the App Secret.
|
|
135
135
|
|
|
136
136
|
| | Grace period (default) | Offline license file |
|
|
137
137
|
| --- | --- | --- |
|
|
@@ -146,13 +146,13 @@ from authforge import AuthForgeClient
|
|
|
146
146
|
|
|
147
147
|
client = AuthForgeClient(
|
|
148
148
|
app_id="YOUR_APP_ID",
|
|
149
|
-
app_secret=
|
|
149
|
+
app_secret=None, # login_from_file does not use the App Secret; do not ship it in air-gapped builds
|
|
150
150
|
public_key="YOUR_PUBLIC_KEY",
|
|
151
151
|
on_failure=lambda reason, exc: print(reason, exc),
|
|
152
152
|
)
|
|
153
153
|
|
|
154
|
-
# 1.
|
|
155
|
-
|
|
154
|
+
# 1. Write an activation request the operator drops into the mint dialog:
|
|
155
|
+
client.write_activation_request("machine.authforge-request")
|
|
156
156
|
|
|
157
157
|
# 2. Later, authorize from the minted file (path or armored text). No network.
|
|
158
158
|
if client.login_from_file("license.authforge"):
|
|
@@ -204,6 +204,8 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
204
204
|
| `get_offline_license()` | `dict \| None` | Metadata of the offline file in use (`jti`, `expires_at`, `hwid_policy`, …) |
|
|
205
205
|
| `get_session_kind()` | `"online" \| "offline" \| None` | Which kind of session the client holds (`None` when logged out) |
|
|
206
206
|
| `get_hwid()` | `str` | The HWID this client sends (or `hwid_override`); customers share it to receive a bound file |
|
|
207
|
+
| `create_activation_request(**kwargs)` | `str` | Unsigned `.authforge-request` for this machine. No network, no secret. Hostname omitted unless `include_machine_name=True` |
|
|
208
|
+
| `write_activation_request(path, **kwargs)` | `None` | Writes that file as UTF-8 |
|
|
207
209
|
| `logout()` | `None` | Stops background checks and clears all session/auth state |
|
|
208
210
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
209
211
|
| `get_session_data()` | `dict \| None` | Full decoded payload map |
|
|
@@ -80,6 +80,21 @@ OFFLINE_LICENSE_ERRORS = (
|
|
|
80
80
|
"hwid_mismatch",
|
|
81
81
|
)
|
|
82
82
|
|
|
83
|
+
# Activation requests (`.authforge-request`): unsigned transport for a HWID.
|
|
84
|
+
# Distinct markers from BEGIN AUTHFORGE LICENSE. Not signed; the Checksum
|
|
85
|
+
# header is the only integrity check. Keep _SDK_TAG in sync with pyproject.toml.
|
|
86
|
+
_ACTIVATION_REQUEST_VERSION = 1
|
|
87
|
+
_ACTIVATION_REQUEST_TYP = "authforge-activation-request"
|
|
88
|
+
_BEGIN_ACTIVATION_REQUEST = "-----BEGIN AUTHFORGE ACTIVATION REQUEST-----"
|
|
89
|
+
_END_ACTIVATION_REQUEST = "-----END AUTHFORGE ACTIVATION REQUEST-----"
|
|
90
|
+
_SDK_TAG = "python/1.2.1"
|
|
91
|
+
_ARMOR_LINE_WIDTH = 64
|
|
92
|
+
_MAX_REQUEST_HWID = 256
|
|
93
|
+
_MAX_REQUEST_MACHINE_NAME = 128
|
|
94
|
+
_MAX_REQUEST_OS = 64
|
|
95
|
+
_MAX_REQUEST_SDK = 64
|
|
96
|
+
_MAX_REQUEST_LICENSE_KEY = 64
|
|
97
|
+
|
|
83
98
|
|
|
84
99
|
class OfflineLicense(TypedDict):
|
|
85
100
|
app_id: str
|
|
@@ -117,6 +132,131 @@ class ParsedLicenseFile(TypedDict):
|
|
|
117
132
|
signature_base64: str
|
|
118
133
|
|
|
119
134
|
|
|
135
|
+
def _clip_request_field(value: str, max_len: int) -> str:
|
|
136
|
+
return value if len(value) <= max_len else value[:max_len]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _json_escape_request(value: str) -> str:
|
|
140
|
+
out: List[str] = []
|
|
141
|
+
for ch in value:
|
|
142
|
+
code = ord(ch)
|
|
143
|
+
if ch == "\\":
|
|
144
|
+
out.append("\\\\")
|
|
145
|
+
elif ch == '"':
|
|
146
|
+
out.append('\\"')
|
|
147
|
+
elif ch == "\b":
|
|
148
|
+
out.append("\\b")
|
|
149
|
+
elif ch == "\f":
|
|
150
|
+
out.append("\\f")
|
|
151
|
+
elif ch == "\n":
|
|
152
|
+
out.append("\\n")
|
|
153
|
+
elif ch == "\r":
|
|
154
|
+
out.append("\\r")
|
|
155
|
+
elif ch == "\t":
|
|
156
|
+
out.append("\\t")
|
|
157
|
+
elif code < 0x20:
|
|
158
|
+
out.append(f"\\u00{code:02x}")
|
|
159
|
+
else:
|
|
160
|
+
out.append(ch)
|
|
161
|
+
return '"' + "".join(out) + '"'
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _wrap_armor_64(value: str) -> str:
|
|
165
|
+
return "\n".join(value[i : i + _ARMOR_LINE_WIDTH] for i in range(0, len(value), _ARMOR_LINE_WIDTH))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _canonical_activation_request_json(
|
|
169
|
+
*,
|
|
170
|
+
app_id: str,
|
|
171
|
+
hwid: str,
|
|
172
|
+
created_at: str,
|
|
173
|
+
machine_name: Optional[str] = None,
|
|
174
|
+
os_name: Optional[str] = None,
|
|
175
|
+
sdk: Optional[str] = None,
|
|
176
|
+
license_key: Optional[str] = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
parts = [
|
|
179
|
+
f'"v":{_ACTIVATION_REQUEST_VERSION}',
|
|
180
|
+
f'"typ":{_json_escape_request(_ACTIVATION_REQUEST_TYP)}',
|
|
181
|
+
f'"appId":{_json_escape_request(app_id)}',
|
|
182
|
+
f'"hwid":{_json_escape_request(_clip_request_field(hwid, _MAX_REQUEST_HWID))}',
|
|
183
|
+
f'"createdAt":{_json_escape_request(created_at)}',
|
|
184
|
+
]
|
|
185
|
+
if machine_name:
|
|
186
|
+
parts.append(
|
|
187
|
+
f'"machineName":{_json_escape_request(_clip_request_field(machine_name, _MAX_REQUEST_MACHINE_NAME))}'
|
|
188
|
+
)
|
|
189
|
+
if os_name:
|
|
190
|
+
parts.append(f'"os":{_json_escape_request(_clip_request_field(os_name, _MAX_REQUEST_OS))}')
|
|
191
|
+
if sdk:
|
|
192
|
+
parts.append(f'"sdk":{_json_escape_request(_clip_request_field(sdk, _MAX_REQUEST_SDK))}')
|
|
193
|
+
if license_key:
|
|
194
|
+
parts.append(
|
|
195
|
+
f'"licenseKey":{_json_escape_request(_clip_request_field(license_key, _MAX_REQUEST_LICENSE_KEY))}'
|
|
196
|
+
)
|
|
197
|
+
return "{" + ",".join(parts) + "}"
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _utc_iso_ms(value: Optional[datetime] = None) -> str:
|
|
201
|
+
now = value or datetime.now(timezone.utc)
|
|
202
|
+
if now.tzinfo is None:
|
|
203
|
+
now = now.replace(tzinfo=timezone.utc)
|
|
204
|
+
now = now.astimezone(timezone.utc)
|
|
205
|
+
return now.strftime("%Y-%m-%dT%H:%M:%S.") + f"{int(now.microsecond / 1000):03d}Z"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _detect_os_label() -> str:
|
|
209
|
+
system = platform.system()
|
|
210
|
+
release = platform.release()
|
|
211
|
+
if system == "Darwin":
|
|
212
|
+
mac = platform.mac_ver()[0]
|
|
213
|
+
label = f"macOS {mac or release}"
|
|
214
|
+
elif system == "Windows":
|
|
215
|
+
label = f"Windows {release}"
|
|
216
|
+
elif system == "Linux":
|
|
217
|
+
label = f"Linux {release}"
|
|
218
|
+
else:
|
|
219
|
+
label = f"{system} {release}".strip()
|
|
220
|
+
return _clip_request_field(label, _MAX_REQUEST_OS)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def format_activation_request(
|
|
224
|
+
*,
|
|
225
|
+
app_id: str,
|
|
226
|
+
hwid: str,
|
|
227
|
+
created_at: str,
|
|
228
|
+
machine_name: Optional[str] = None,
|
|
229
|
+
os: Optional[str] = None,
|
|
230
|
+
sdk: Optional[str] = None,
|
|
231
|
+
license_key: Optional[str] = None,
|
|
232
|
+
) -> str:
|
|
233
|
+
"""Armored ``.authforge-request`` text from explicit fields."""
|
|
234
|
+
json_body = _canonical_activation_request_json(
|
|
235
|
+
app_id=app_id,
|
|
236
|
+
hwid=hwid,
|
|
237
|
+
created_at=created_at,
|
|
238
|
+
machine_name=machine_name,
|
|
239
|
+
os_name=os,
|
|
240
|
+
sdk=sdk,
|
|
241
|
+
license_key=license_key,
|
|
242
|
+
)
|
|
243
|
+
payload_b64 = base64.b64encode(json_body.encode("utf-8")).decode("ascii")
|
|
244
|
+
checksum = hashlib.sha256(payload_b64.encode("utf-8")).hexdigest()[:16]
|
|
245
|
+
clean_app = app_id.replace("\r", " ").replace("\n", " ").strip()
|
|
246
|
+
return "\n".join(
|
|
247
|
+
[
|
|
248
|
+
_BEGIN_ACTIVATION_REQUEST,
|
|
249
|
+
f"Version: {_ACTIVATION_REQUEST_VERSION}",
|
|
250
|
+
f"App-Id: {clean_app}",
|
|
251
|
+
f"Checksum: {checksum}",
|
|
252
|
+
"",
|
|
253
|
+
_wrap_armor_64(payload_b64),
|
|
254
|
+
_END_ACTIVATION_REQUEST,
|
|
255
|
+
"",
|
|
256
|
+
]
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
120
260
|
def parse_license_file(text: str) -> Optional[ParsedLicenseFile]:
|
|
121
261
|
"""Parse armored ``.authforge`` text.
|
|
122
262
|
|
|
@@ -360,7 +500,7 @@ class AuthForgeClient:
|
|
|
360
500
|
def __init__(
|
|
361
501
|
self,
|
|
362
502
|
app_id: str,
|
|
363
|
-
app_secret: str,
|
|
503
|
+
app_secret: Optional[str],
|
|
364
504
|
public_key: PublicKeyArg,
|
|
365
505
|
heartbeat_mode: Optional[str] = None,
|
|
366
506
|
heartbeat_interval: int = 900,
|
|
@@ -374,8 +514,12 @@ class AuthForgeClient:
|
|
|
374
514
|
) -> None:
|
|
375
515
|
if not app_id or not isinstance(app_id, str):
|
|
376
516
|
raise ValueError("app_id must be a non-empty string")
|
|
377
|
-
|
|
378
|
-
|
|
517
|
+
# Empty/None is valid for offline-only clients (login_from_file).
|
|
518
|
+
# Online APIs (login, validate_license, self_ban) still require a secret.
|
|
519
|
+
if app_secret is None:
|
|
520
|
+
app_secret = ""
|
|
521
|
+
elif not isinstance(app_secret, str):
|
|
522
|
+
raise ValueError("app_secret must be a string or None")
|
|
379
523
|
public_key_list = self._normalize_public_key_list(public_key)
|
|
380
524
|
if not public_key_list:
|
|
381
525
|
raise ValueError(
|
|
@@ -459,6 +603,49 @@ class AuthForgeClient:
|
|
|
459
603
|
"""
|
|
460
604
|
return self._hwid
|
|
461
605
|
|
|
606
|
+
def create_activation_request(
|
|
607
|
+
self,
|
|
608
|
+
*,
|
|
609
|
+
include_machine_name: bool = False,
|
|
610
|
+
machine_name: Optional[str] = None,
|
|
611
|
+
os: Optional[str] = None,
|
|
612
|
+
omit_os: bool = False,
|
|
613
|
+
sdk: Optional[str] = None,
|
|
614
|
+
omit_sdk: bool = False,
|
|
615
|
+
license_key: Optional[str] = None,
|
|
616
|
+
created_at: Optional[str] = None,
|
|
617
|
+
) -> str:
|
|
618
|
+
"""Build an activation request (``.authforge-request``) for this machine.
|
|
619
|
+
|
|
620
|
+
No network, no session, no app secret. The HWID is the same value
|
|
621
|
+
:meth:`login` / :meth:`login_from_file` use. ``machine_name`` is omitted
|
|
622
|
+
unless ``include_machine_name`` is true (hostnames are often a person's
|
|
623
|
+
name).
|
|
624
|
+
"""
|
|
625
|
+
created = created_at if created_at else _utc_iso_ms()
|
|
626
|
+
name: Optional[str] = None
|
|
627
|
+
if include_machine_name:
|
|
628
|
+
name = machine_name or platform.node() or socket.gethostname()
|
|
629
|
+
os_name: Optional[str] = None if omit_os else (os if os is not None else _detect_os_label())
|
|
630
|
+
sdk_tag: Optional[str] = None if omit_sdk else (sdk if sdk is not None else _SDK_TAG)
|
|
631
|
+
key = license_key if license_key is not None else self._license_key
|
|
632
|
+
return format_activation_request(
|
|
633
|
+
app_id=self.app_id,
|
|
634
|
+
hwid=self._hwid,
|
|
635
|
+
created_at=created,
|
|
636
|
+
machine_name=name,
|
|
637
|
+
os=os_name,
|
|
638
|
+
sdk=sdk_tag,
|
|
639
|
+
license_key=key,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
def write_activation_request(self, path: str, **kwargs: Any) -> None:
|
|
643
|
+
"""Write an activation request to ``path`` (UTF-8). Same kwargs as
|
|
644
|
+
:meth:`create_activation_request`."""
|
|
645
|
+
text = self.create_activation_request(**kwargs)
|
|
646
|
+
with open(path, "w", encoding="utf-8", newline="\n") as handle:
|
|
647
|
+
handle.write(text)
|
|
648
|
+
|
|
462
649
|
def login_from_file(self, path_or_text: str) -> bool:
|
|
463
650
|
"""Authorize from a cloud-minted offline license file (``.authforge``)
|
|
464
651
|
with NO network access. Accepts a filesystem path or the armored text.
|
|
@@ -558,9 +745,16 @@ class AuthForgeClient:
|
|
|
558
745
|
except Exception:
|
|
559
746
|
pass
|
|
560
747
|
|
|
748
|
+
def _require_app_secret(self) -> None:
|
|
749
|
+
if not self.app_secret:
|
|
750
|
+
raise ValueError(
|
|
751
|
+
"app_secret is required for online APIs; omit it only when using login_from_file"
|
|
752
|
+
)
|
|
753
|
+
|
|
561
754
|
def login(self, license_key: str) -> bool:
|
|
562
755
|
if not license_key or not isinstance(license_key, str):
|
|
563
756
|
raise ValueError("license_key must be a non-empty string")
|
|
757
|
+
self._require_app_secret()
|
|
564
758
|
|
|
565
759
|
try:
|
|
566
760
|
self._validate_and_store(license_key)
|
|
@@ -575,6 +769,7 @@ class AuthForgeClient:
|
|
|
575
769
|
session state or starting the heartbeat thread."""
|
|
576
770
|
if not license_key or not isinstance(license_key, str):
|
|
577
771
|
raise ValueError("license_key must be a non-empty string")
|
|
772
|
+
self._require_app_secret()
|
|
578
773
|
try:
|
|
579
774
|
body: Dict[str, Any] = {
|
|
580
775
|
"appId": self.app_id,
|
|
@@ -661,6 +856,7 @@ class AuthForgeClient:
|
|
|
661
856
|
resolved_license = explicit_license or current_license
|
|
662
857
|
if not resolved_license:
|
|
663
858
|
raise ValueError("missing_license_key")
|
|
859
|
+
self._require_app_secret()
|
|
664
860
|
|
|
665
861
|
body = {
|
|
666
862
|
"appId": self.app_id,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: authforge-sdk
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.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
|
|
@@ -110,7 +110,7 @@ client = AuthForgeClient(
|
|
|
110
110
|
| Parameter | Type | Default | Description |
|
|
111
111
|
|---|---|---|---|
|
|
112
112
|
| `app_id` | str | required | Your application ID from the AuthForge dashboard |
|
|
113
|
-
| `app_secret` | str | required | Your application secret from the AuthForge dashboard |
|
|
113
|
+
| `app_secret` | str | required for online APIs; `None` / `""` for `login_from_file` only | Your application secret from the AuthForge dashboard. Do not ship it in air-gapped binaries. |
|
|
114
114
|
| `public_key` | `str \| Sequence[str]` | required | App Ed25519 public key(s) (base64) from dashboard. Pass one key, a list of keys, or a comma-separated string to trust multiple keys during rotation (see [Key rotation](#key-rotation)). |
|
|
115
115
|
| `heartbeat_mode` | `str \| None` | `None` | Deprecated shim (see [Migrating from heartbeat_mode](#migrating-from-heartbeat_mode)). Use `online_heartbeat` instead. |
|
|
116
116
|
| `heartbeat_interval` | int | `900` | Seconds between background checks (minimum `10`; default 15 min). Applies to both grace period checks and online check-ins. |
|
|
@@ -159,7 +159,7 @@ Tune `ttl_seconds` to set the grace period duration (server default 24h, clamped
|
|
|
159
159
|
|
|
160
160
|
## Offline license files (`.authforge`)
|
|
161
161
|
|
|
162
|
-
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins.
|
|
162
|
+
For machines that never connect to the internet, the operator mints a **signed offline license file** in the AuthForge dashboard (License page -> *Mint .authforge file*) or via `POST /v1/licenses/{licenseKey}/offline-files`. The file is a standalone Ed25519-signed document; the SDK verifies it with **only** your app public key and the machine HWID. It never contacts AuthForge and never starts online check-ins. Pass `app_secret=None` (or `""`) so the air-gapped binary does not contain the App Secret.
|
|
163
163
|
|
|
164
164
|
| | Grace period (default) | Offline license file |
|
|
165
165
|
| --- | --- | --- |
|
|
@@ -174,13 +174,13 @@ from authforge import AuthForgeClient
|
|
|
174
174
|
|
|
175
175
|
client = AuthForgeClient(
|
|
176
176
|
app_id="YOUR_APP_ID",
|
|
177
|
-
app_secret=
|
|
177
|
+
app_secret=None, # login_from_file does not use the App Secret; do not ship it in air-gapped builds
|
|
178
178
|
public_key="YOUR_PUBLIC_KEY",
|
|
179
179
|
on_failure=lambda reason, exc: print(reason, exc),
|
|
180
180
|
)
|
|
181
181
|
|
|
182
|
-
# 1.
|
|
183
|
-
|
|
182
|
+
# 1. Write an activation request the operator drops into the mint dialog:
|
|
183
|
+
client.write_activation_request("machine.authforge-request")
|
|
184
184
|
|
|
185
185
|
# 2. Later, authorize from the minted file (path or armored text). No network.
|
|
186
186
|
if client.login_from_file("license.authforge"):
|
|
@@ -232,6 +232,8 @@ A desktop app running 6h/day with online check-ins at a 15-minute interval burns
|
|
|
232
232
|
| `get_offline_license()` | `dict \| None` | Metadata of the offline file in use (`jti`, `expires_at`, `hwid_policy`, …) |
|
|
233
233
|
| `get_session_kind()` | `"online" \| "offline" \| None` | Which kind of session the client holds (`None` when logged out) |
|
|
234
234
|
| `get_hwid()` | `str` | The HWID this client sends (or `hwid_override`); customers share it to receive a bound file |
|
|
235
|
+
| `create_activation_request(**kwargs)` | `str` | Unsigned `.authforge-request` for this machine. No network, no secret. Hostname omitted unless `include_machine_name=True` |
|
|
236
|
+
| `write_activation_request(path, **kwargs)` | `None` | Writes that file as UTF-8 |
|
|
235
237
|
| `logout()` | `None` | Stops background checks and clears all session/auth state |
|
|
236
238
|
| `is_authenticated()` | `bool` | True when an active authenticated session exists |
|
|
237
239
|
| `get_session_data()` | `dict \| None` | Full decoded payload map |
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "authforge-sdk"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.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
|