- HWID v2: the HWID is derived from the OS machine id.
sha256("authforge-hwid-v2|" + app_id + "|" + machine_id)as lowercase hex, wheremachine_idis the trimmed, lowercased OS id: macOSIOPlatformUUID(from/usr/sbin/ioreg), Linux/etc/machine-idthen/var/lib/dbus/machine-id, WindowsHKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid(64-bit registry view, viawinreg). The previous MAC + CPU + disk serial fingerprint changed whenever the first network interface changed (macOS rotates private Wi-Fi addresses per network); it is kept only as a fallback, aslegacy:<old fingerprint>, when no valid OS id can be read. - Every bound HWID changes once on upgrade. Online, a device rebinds into a free HWID slot, or the license needs an HWID reset. Offline
.authforgefiles bound to a pre-2.0 HWID fail withhwid_mismatchand must be re-issued (collect the new HWID withcreate_activation_request()). - HWIDs are now identical across all AuthForge SDKs for the same app id and machine, and different between apps on the same machine. Containers without a unique
/etc/machine-idshould passhwid_override. hwid_overridebehaves as before.login()fireson_failureonce on a network failure. When/auth/validateis still unreachable after the network retry,login()now callson_failure("login_failed", exc)once, withexc.codenetwork_errorortimeout, and returnsFalse. Previously it calledon_failure("network_error", exc)and thenon_failure("login_failed", exc). Without a callback the process still ends withos._exit(1), now onlogin_failed. Matches check-ins (1.4.0), the C++ SDK and the C# SDK'sLogin(). Callers that react to thenetwork_errorreason should checkexc.codeonlogin_failedinstead.self_ban()reports a network failure once, by raising. When/auth/selfbanis still unreachable after the network retry,self_ban()raisesAuthForgeErrorwithexc.codenetwork_errorortimeoutand never callson_failureoros._exit. Previously it calledon_failure("network_error", exc)(or, without a callback,os._exit(1)) and then raised. Server rejections already only raised. Matchesactivate_offline(). The SDK no longer passes anetwork_errorreason toon_failureanywhere.
- Module-level helpers
hwid_from_machine_id,normalize_machine_id,parse_ioreg_platform_uuid,parse_reg_query_machine_guid. - Tests run the shared cross-SDK conformance vectors
hwid_vectors.json(generated byauthforge-node/generate_hwid_vectors.mjs), plus a macOS-only live check againstioreg.
- Self-serve offline activation:
AuthForgeClient.activate_offline(license_key, path=None) -> boolcallsPOST /auth/offline/activateonce and turns a license key into a lifetime.authforgefile bound to this machine's HWID (the same HWIDlogin()sends). The SDK verifies the file locally (public key(s), app id, HWID) before writing it, writes it byte-for-byte and atomically topath(creating parent directories), and authenticates the client exactly aslogin_from_file()does.get_offline_license()["replay"]isTruewhen the server returned a file the machine already had. Needs the App Secret. Billing: 1 new offline activation = 2 credits for the app owner; re-activating the same machine is free. - On failure
activate_offline()raisesAuthForgeError, writes nothing (an existing file atpathis left untouched), does not authenticate the client (an existing session is kept) and never callson_failureoros._exit. An empty license key, then a missing App Secret, raise the usualValueErrorbefore any request. - New server codes, all fatal and added to
KNOWN_SERVER_ERRORSandDEFINITIVE_ERROR_CODES:offline_activation_disabled,offline_activation_requires_perpetual,offline_activation_requires_seats,offline_activation_limit_reached. - New SDK codes:
offline_file_rejected(fatal, also listed inKNOWN_SERVER_ERRORSandDEFINITIVE_ERROR_CODES; the returned file failed local verification, the verifier's reason is in the message) andfile_write_failed(transient; the verified file could not be written). A success response without afileraisesunexpected_response(transient). - Tests run the shared
self_serve_activation_vectors.jsonconformance vectors.
- Without an
on_failurecallback, a transient background check failure (network outage, timeout,rate_limited,system_error,no_credits, ...) no longer ends the process. The SDK prints one line to stderr,AuthForge: background check failed (<code>); retrying next interval, keeps the session and checks in again on the next interval. Previously a brief outage killed any app that enabled online check-ins without setting a callback. - Unchanged: without a callback, definitive failures (including the
session_expireda transient failure becomes once the session TTL has passed) and failedlogin()calls still callos._exit(1).
- The README and
AGENTS.mdexamples no longer callos._exit(1)fromon_failure. They set athreading.Eventthat the main thread checks so it can save work and exit, mention_thread.interrupt_main()for blocked main threads, and keepos._exitas a last resort after saving.
on_failure("heartbeat_failed", exc)now always receives anAuthForgeError(aValueErrorsubclass) withexc.code,exc.transientandexc.fatal. For server codesstr(exc)equals the code.is_transient_error()andDEFINITIVE_ERROR_CODESare exported;TRANSIENT_ERROR_CODESlists the named transient codes for reference.- Classification: only
revoked,expired,hwid_mismatch,blocked,session_expired,malformed_request,app_disabled,invalid_appandsignature_mismatchare definitive. Everything else is transient, includingno_credits,demo_quota_exceeded,app_burn_cap_reached,bad_request,invalid_key, everyhttp_error_<status>and unknown codes. - Transient heartbeat failures are reported and background checks keep running on the next interval. Previously the loop stopped after any failure. After the session TTL passes, the next transient failure is reported as a definitive
session_expired; grace-period expiry also reportssession_expired. - Definitive heartbeat failures clear the stored session (as
logout()does) and stop background checks beforeon_failureruns. Previously the session stayed authenticated. - New code
unexpected_response(transient): a failed check-in whose body is not a JSON object with"status": "failed"and a non-emptyerrorstring. The message includes the rawstatusanderror. - Request retries: only
rate_limited, or HTTP 429 with no error code, is retried (after 2s, then 5s).no_credits,demo_quota_exceededandapp_burn_cap_reachedon HTTP 429 are no longer retried. This also applies tologin()andvalidate_license(). - Heartbeat network failures no longer also fire
on_failure("network_error", ...); they are reported once asheartbeat_failedwith codenetwork_errorortimeout. - Server codes this SDK doesn't know are passed through as-is instead of becoming
unknown_error(also invalidate_license()results). - HTTP errors with a non-JSON body (
http_error_<status>), network failures and unparseable responses now raiseAuthForgeErrorinstead ofRuntimeError/ plainValueError. on_failureruns with no SDK lock held, so it may calllogout(),is_authenticated()orlogin(). A check-in still in flight duringlogout()orlogin()is discarded instead of restoring the old session.