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