velsigil-client (Python)
Official Python client for the Velsigil license server. It validates license keys, binds them to the device, keeps an offline lease for when the server can't be reached, and checks for and downloads updates. Every response that can report success is Ed25519-signed by your Velsigil server and verified by the SDK before it is used.
- Python 3.10+ on Windows, macOS and Linux (releases are tested on 3.10 and 3.14)
- One runtime dependency:
cryptography(≥ 50.0.0, the first release with no published advisory).PyNaCl(≥ 1.6.2) is an optional fallback. - HTTP uses the standard library (
urllib), so there is norequestsdependency
Contents
- Installation
- Quick start
- How it works
- API reference
- Result codes
- In-app free trials
- Offline leases
- Device secret persistence
- Updates and downloads
- Thread safety
- Hardening your integration
- Development and tests
Installation
pip install velsigil-client # cryptography backend
pip install "velsigil-client[nacl]" # also install the PyNaCl fallback backend
From a checkout of the SDK repository: pip install ./python.
Quick start
Get the API URL, product id and public key from the product's Integration tab in the Velsigil panel.
from velsigil_client import Code, FileStore, VelsigilClient, default_store_path
# Keep these as constants in your code (see "Hardening").
API_URL = "https://licenses.example.com"
PRODUCT_ID = "0b9f4c1e-8d6a-4f7e-9c3b-2a1d5e6f7a8b"
PUBLIC_KEY = "<your product's public key>" # panel: Products > your product > Integration
client = VelsigilClient(
API_URL,
PRODUCT_ID,
PUBLIC_KEY,
store=FileStore(default_store_path("MyApp")), # persists device secret + offline lease
)
result = client.validate_with_offline_fallback(license_key, version="1.2.0")
if not result.ok:
show_error(result.code, result.message) # e.g. "license_expired"
raise SystemExit(1)
if result.has_feature("export"):
enable_export()
print("Plan:", result.license.plan, "- days left:", result.days_remaining())
There is a complete, runnable program in examples/basic.py.
Set the constants API_URL, PRODUCT_ID and PUBLIC_KEY at its top to your product's values (they ship as
placeholders; until all three are set it prints a usage message and exits with code 2), then run:
python examples/basic.py --license-key VSG-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
python examples/basic.py --license-key ... --download ./downloads/app.zip
python examples/basic.py --license-key ... --deactivate
The values are constants in code, as in your application, not read from the environment. For local testing
only, the environment variables VELSIGIL_API_URL, VELSIGIL_PRODUCT_ID and VELSIGIL_PUBLIC_KEY override
them when the API URL is loopback (localhost, 127.0.0.1, [::1], for example a panel dev server on
http://localhost:3000); for any other server they are ignored.
How it works
Each request carries the product id, a fresh 32-byte random nonce (base64url, 43 characters) and a unix timestamp (local clock plus any offset learned from the server). Requests that involve a license also send the hardware id and, once issued, the device secret.
The server answers HTTP 200 with a signed envelope { data, sig, kid }. The SDK:
- verifies the Ed25519 signature over the exact ASCII bytes of
data, using only the public key you passed to the constructor (thekidfield is ignored, so the key can't be swapped); - only then base64url-decodes and parses the JSON payload;
- rejects the payload unless it echoes this request's nonce, this product id and the expected response type. That stops replayed or redirected responses;
- returns a
VelsigilResult.ok=Trueis only possible after all three steps succeed.
Unsigned HTTP errors (rate limiting, validation errors, blocked IPs, server errors) become
failure results and can never produce ok=True. If the server reports a signed
clock_skew, the SDK learns offset = serverTime - localTime and retries once with a new
nonce.
Request methods never raise. Licensing problems and transport failures come back as a result
with ok=False and a code. Exceptions are only raised by the constructor (bad configuration),
by download_to_file (file errors) and by the low-level verification helpers.
API reference
VelsigilClient(api_url, product_id, public_key, *, ...)
| Parameter | Default | Description |
|---|---|---|
api_url |
— | Server origin, e.g. https://licenses.example.com. /api/client/v1 is appended unless already present; a sub-path such as https://example.com/velsigil also works. Must be https://. Plain http:// is only accepted for localhost, 127.0.0.1 and ::1. URLs containing credentials, a query string or a fragment are rejected. |
product_id |
— | Product UUID (case-insensitive). |
public_key |
— | Product Ed25519 public key: standard base64 of the raw 32-byte key. Missing padding is tolerated. The public test keys of the SDK test vectors are refused unless api_url is a loopback host. |
timeout |
15.0 |
Seconds per request. Applies to connect and read, plus an overall deadline for the response body. |
hwid |
auto | Overrides the hardware id (8–256 printable characters). The default comes from get_hardware_id(). |
store |
MemoryStore() |
A LicenseStore that persists the device secret and offline lease per product. Use FileStore (or your own store) in production. |
allow_insecure_http |
False |
Allows plain HTTP to non-local hosts. Never enable this in production. |
ssl_context |
system CAs | A custom ssl.SSLContext, for example one trusting a private CA. It must keep certificate and hostname verification on. |
user_agent |
velsigil-client-python/<v> |
Overrides the User-Agent header. |
clock |
time.time |
Callable that returns unix seconds. Useful for tests. |
crypto_backend |
"auto" |
"auto" (cryptography, then PyNaCl), "cryptography" or "nacl". |
The constructor raises ConfigurationError for invalid arguments, CryptoBackendError when no
Ed25519 backend is installed, and HardwareIdError when the machine id can't be read and no
hwid was given.
Methods
| Method | Description |
|---|---|
validate(license_key, version=None, device_name=None) |
Validates the key and activates this device on first use. On success it stores the newly issued device secret and the offline lease. On a definitive signed denial (see Offline leases) it deletes the stored lease. |
start_trial(version=None, device_name=None, email=None) |
Starts a free trial of the product on this device without a license key (see In-app free trials). On ok, result.trial_key is the new key: store it, then use validate(). Stores the device secret and lease like validate(). email (up to 254 characters) is sent only when given. Returns already_licensed locally (nothing sent, nothing changed) when a device secret or lease is already stored for the product, and store_unavailable when the store cannot be read. |
deactivate(license_key) |
Releases this device's activation slot. On ok (or device_not_found) it clears the stored device secret and lease. |
check_update(current_version=None) |
Returns result.update (UpdateInfo). The code is no_release when nothing is published. |
get_download(license_key, version=None) |
Returns result.download (DownloadInfo) with a short-lived link. The device must already be activated when the product locks HWIDs. |
download_to_file(download, destination) |
Streams the release to destination and checks its signed size and SHA-256 before moving it into place. Returns the absolute path; raises DownloadError on failure. |
validate_offline() |
Checks the stored lease without network access. Result has offline=True. |
validate_with_offline_fallback(license_key, version=None, device_name=None) |
Calls validate() first. Only if it returns network_error does it fall back to validate_offline(). |
clear_stored_state() |
Forgets the stored device secret and lease for this product. |
VelsigilClient.get_hardware_id() (static) |
This machine's HWID (see below). |
Properties: api_url (normalised), product_id, hwid, clock_offset (seconds learned from a
signed clock_skew response), key_id (diagnostics only).
VelsigilResult
| Member | Description |
|---|---|
ok: bool |
True only for a verified success. |
code: str |
"ok" or a failure code (see Result codes). |
message: str |
Server message for signed responses; fixed SDK text otherwise. Unsigned server text is never echoed. |
license: LicenseInfo | None |
id, plan, status, features, expires_at, max_devices, devices_used, created_at, has_feature(name). On a failed result (license_revoked, license_expired, ...) license.features is informational only (servers since 2026-10-06 send it empty) and license.has_feature() is False; gate features with result.has_feature(). |
activation: ActivationInfo | None |
id, status, first_seen_at, device_secret_issued. The secret itself is never exposed. |
lease: LeaseInfo | None |
token, expires_at. |
update: UpdateInfo | None |
latest_version, min_version, update_available, mandatory, changelog. |
download: DownloadInfo | None |
url (hidden from repr), expires_at, file_name, size, sha256, version. |
request_id: str | None |
Server request id. Quote it in support requests. |
offline: bool |
True when the result comes from the stored lease. |
server_time, http_status, retry_after |
Diagnostics. retry_after is set for rate_limited. |
trial_key: str | None |
The key of the trial start_trial() just started (ok results of start_trial only; hidden from repr). Sent once: store it immediately. |
has_feature(name) |
True only if ok and the license includes name. |
features |
Tuple of features; empty unless ok. |
expires_at, expires_at_datetime, is_lifetime |
License expiry as unix seconds or an aware UTC datetime. None means lifetime or unknown. |
is_trial |
True for a free-trial license (the optional signed trial field, online and offline; also license.is_trial). Pair it with days_remaining() for "Trial: N days left" and a "Buy now" link; a purchase with the same e-mail keeps the key and turns it False at the next online validation. |
trial_ref |
The trial's conversion reference (servers since 2026-10-06; also license.trial_ref): set on online results for a free trial a purchase can still convert, also on license_expired; None otherwise and offline. Put it in your "Buy now" link with result.with_trial_ref(buy_url) (or velsigil_client.with_trial_ref(url, ref)): it appends velsigil_trial=<ref> (a Stripe Payment Link gets client_reference_id=), and the purchase then converts THIS trial (same key) whatever e-mail address the buyer pays with. Opaque; never store it. |
seconds_remaining(now=None), days_remaining(now=None), is_expired(now=None) |
Expiry helpers; remaining time never goes below 0. seconds_remaining and is_expired default to the local clock. days_remaining rounds up and, without now, measures at the result's own time (the signed server_time online, the time of the check offline, kept in reference_time): an N-day trial shows N right after start_trial(), 1 on its last day and 0 once expired. The same rule in every Velsigil SDK. |
All timestamps are unix seconds in UTC, exactly as signed by the server.
Stores
| Class | Description |
|---|---|
LicenseStore |
Abstract interface: load(product_id) -> StoredState, save(product_id, state), delete(product_id). Implementations must be thread-safe. |
MemoryStore() |
Keeps state in the process. It is lost on exit. |
FileStore(path) |
JSON file holding every product. Atomic writes (temp file, then fsync, then os.replace). On POSIX the directory is 0700 and the file 0600. A corrupt file is treated as empty. Every FileStore on the same path in a process shares one lock, so several instances (for example one per product on default_store_path()) never lose each other's updates; sharing one instance is still simplest. Separate processes are not coordinated (see Thread safety). |
default_store_path(app_name) |
Per-user path: %LOCALAPPDATA%\<app>\Velsigil\license.json on Windows, ~/Library/Application Support/<app>/Velsigil/license.json on macOS, $XDG_DATA_HOME/<app>/velsigil/license.json on Linux. Upgrading from an SDK released under the former product name (Veltrix): until the first write, a FileStore at this path reads the file left in the old Veltrix/veltrix directory, so installed apps keep their device secret and offline lease. |
StoredState(device_secret=None, lease_token=None) |
Immutable value. Its repr hides the secret. |
Hardware id
hwid = sha256_hex("vx-hwid-v1:" + machine_id.strip().lower()). The machine id comes from:
- Windows:
HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid, read from the 64-bit registry view - Linux:
/etc/machine-id, then/var/lib/dbus/machine-id(a file that is empty or holds systemd's placeholderuninitializedis skipped) - macOS:
IOPlatformUUIDfrom/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice, run without a shell
hwid_from_machine_id(raw) exposes the derivation, and get_hardware_id() /
velsigil_client.hwid.read_machine_id() expose the platform lookup. Every Velsigil SDK computes the same value. If you pass your own
hwid, derive it from something stable; hashing it with hwid_from_machine_id is a good idea.
Low-level helpers
Ed25519Verifier(public_key), open_envelope(verifier, envelope, expected_nonce, expected_product_id, expected_type, expected_hwid=None),
verify_lease(verifier, token, product_id, hwid, now) and key_id_for(public_key). These raise
EnvelopeError / LeaseError, whose .reason is one of invalid_signature, nonce_mismatch,
product_mismatch, type_mismatch, hwid_mismatch, expired or malformed. expected_type is
required: pass the endpoint the request went to (validate, deactivate, update_check,
download or trial). The server also signs ok: true answers to update_check, which needs no license, so
the type must always be checked; None or an empty value raises ValueError. With expected_hwid
(the hwid the request was sent with), a signed lease or activation.hwidHash bound to another
device raises hwid_mismatch.
These helpers refuse the two public keys of the SDK test vectors (keys.publicKey and
keys.wrongPublicKey in test-vectors.json, whose private keys are published there) on every host: they
have no server URL, so unlike VelsigilClient they make no loopback exception. Ed25519Verifier,
key_id_for and velsigil_client.crypto.decode_public_key refuse the key itself (in any accepted
spelling), open_envelope and verify_lease a verifier that holds it, before anything else is checked.
The refusal is a ConfigurationError (code invalid_configuration, the error of an invalid key) with the
same message as the client constructor's. There is no opt-out argument; the SDK's own test-vector suite
uses internal, unguarded equivalents.
Exceptions
VelsigilError is the base class of ConfigurationError (also a ValueError), CryptoBackendError,
HardwareIdError, StoreError, DownloadError, EnvelopeError and LeaseError.
ConfigurationError.code is "invalid_configuration" and DownloadError.code is one of
download_failed (non-200 status, e.g. an expired link answering 410, or a URL rejected by the HTTPS
policy), integrity_mismatch (size or SHA-256 differs from the signed values; the partial file is
deleted), io_error (the destination could not be written), network_error (transport failure) or
validation_error (not a DownloadInfo). These are the cross-SDK codes of SPEC section 14.
Result codes
| Code | Origin | Meaning / suggested handling |
|---|---|---|
ok |
signed | Success. |
invalid_key |
signed | Key not found for this product. |
license_expired · license_suspended · license_revoked · license_banned |
signed | License state does not allow use. Show the message; renewal or support. |
device_limit_reached |
signed | All device slots in use. The customer can free one in the portal. |
device_revoked |
signed | This device was revoked by the seller. |
device_verification_failed |
signed | Device secret mismatch on a strict product. Reset devices in the portal. |
device_not_found |
signed | deactivate: this device is not registered. |
device_not_activated |
signed | get_download: validate (activate) first. |
activation_rate_limited · activation_cooldown · activations_disabled |
signed | New activations are temporarily not possible. |
downloads_disabled · release_not_found · no_release |
signed | Download or update not available. |
blacklisted |
signed | Device, IP or customer is blocked. |
outdated_version |
signed | Version below the product minimum. result.update tells you what to install. |
product_paused · product_disabled |
signed | Product unavailable (the message is set by the seller). |
clock_skew |
signed | Clock still too far off after the automatic retry. |
replay_detected |
signed | Nonce reuse detected by the server (should not happen with this SDK). |
trial_already_used |
signed | This device already used a free trial of the product. Offer to buy; a stored lease is kept. |
trial_unavailable |
signed | start_trial: the seller offers no in-app trial right now (switched off, today's limit, too many trials from this network). Show the message. |
trial_email_required · trial_email_invalid · trial_email_not_accepted |
signed | start_trial: the offer confirms an e-mail address first: ask for one, a valid one, or a personal / work address (throwaway domains are refused). |
trial_confirmation_sent |
signed | start_trial: not an error of the user. A confirmation link was e-mailed; the key arrives by e-mail and is entered like any key. The same answer for every valid address. |
validation_error |
unsigned 400 | Request rejected. Also returned locally, without a request, for an empty or oversized key, version or device name. |
ip_blocked |
unsigned 403 | Network temporarily blocked. |
unknown_product |
unsigned 404 | Wrong product id or server. |
rate_limited |
unsigned 429 | Back off. result.retry_after holds seconds when sent. |
internal_error |
unsigned 5xx | Server error (also 502/503/504 with a Velsigil error body). |
payload_too_large · unsupported_media_type |
unsigned 413/415 | Should not happen with this SDK (also mapped from a bodiless 413/415, e.g. from a proxy). |
network_error |
SDK | DNS, connect, TLS or timeout failure, or an HTTP 502/503/504 without a Velsigil error body (gateway can't reach the server). This is the only code that triggers the offline fallback. |
invalid_response |
SDK | Signature invalid, nonce/product/type mismatch (the signed type must match the endpoint), a signed lease or activation.hwidHash that belongs to another device (the request was rewritten in transit; nothing from the response is stored), malformed or oversized body, unsigned success, unexpected status (including redirects, which are never followed). Treat it as a failure. |
no_lease · lease_expired · lease_invalid |
SDK | Offline validation: nothing stored, lease past exp, or lease rejected (signature, type, product or HWID). |
invalid_configuration |
SDK | ConfigurationError.code (the constructor raises). |
download_failed · integrity_mismatch · io_error |
SDK | DownloadError.code from download_to_file(). |
panel_too_old |
SDK | start_trial: the Velsigil server has no in-app trial endpoint yet (HTTP 404 with the Velsigil error code not_found). The seller must update the panel. |
already_licensed |
SDK | start_trial: this device already holds a license for the product (a device secret or offline lease is stored), and a trial must not replace it. Nothing was sent and the stored state is unchanged. Validate the saved key instead, or call deactivate(key) / clear_stored_state() first. |
store_unavailable |
SDK | start_trial: the store could not be read (for example a locked file or keyring), so the SDK cannot tell whether this device already holds a license (Code.STORE_UNAVAILABLE). Nothing was sent; try again later. |
The constants live in velsigil_client.Code (for example Code.LICENSE_EXPIRED).
Unsigned errors are mapped the same way in every Velsigil SDK: a known code in a Velsigil error body
({"error": {"code": ...}}) wins; otherwise 400 → validation_error, 413 → payload_too_large,
415 → unsupported_media_type, 429 → rate_limited, 502/503/504 → network_error without a Velsigil
error body or internal_error with one, other 5xx → internal_error, anything else →
invalid_response. The unsigned message text is never shown.
In-app free trials
When the seller turns on In your app in the product's trial offer (panel → Products → your product → Trials), an app that has no license key yet can start a free trial for its device:
saved_key = load_saved_license_key() # your own settings storage
if saved_key is None:
trial = client.start_trial(version="1.2.0", device_name=socket.gethostname())
if trial.ok and trial.trial_key:
save_license_key(trial.trial_key) # FIRST: the server can never send this key again
show_trial_banner(trial.days_remaining()) # trial.is_trial is True
elif trial.code == Code.TRIAL_EMAIL_REQUIRED:
pass # the offer confirms an e-mail address first: ask for it, then start_trial(email=...)
else:
show_message(trial.message) # trial_confirmation_sent, trial_already_used, trial_unavailable, panel_too_old, ...
- One trial per device and product. A device that already had a trial of the product (from the app, the
seller's website or the customer portal) gets
trial_already_used; offer to buy. - The trial key is an ordinary license key on the seller's trial plan. Store it like a key the user typed and
use
validate()from then on. Show it in your About / License screen: the customer needs it for the customer portal, to move the trial to another device and for support. A purchase converts the same key (the seller's staff, or a purchase with the e-mail address the trial was confirmed with). - E-mail confirmation (optional, the seller's choice). Then the first answer is
trial_confirmation_sent; the customer confirms the address from the e-mail, gets the key on the confirmation page and by e-mail, and enters it in the app (validate()). - The SDK stores the trial's device secret and offline lease like after a validation, never the key.
- Never over an existing license. When a device secret or an offline lease is already stored for the product
(the device was activated with a key, paid or trial),
start_trial()returnsalready_licensed(Code.ALREADY_LICENSED) without sending anything and leaves the stored state as it is, so a trial can never replace the device secret and lease of a paid license. Validate the saved key instead; to really start over, calldeactivate(key)(orclear_stored_state()when the key is gone) first. When the store cannot be read,start_trial()returnsstore_unavailableand sends nothing: a failed read is never taken for "nothing stored". - The server limits trials per network, per device and per day, and HWIDs are asserted by the client: a user who changes the machine id can start another trial within those limits. Treat a trial as a marketing tool, not a security boundary.
panel_too_old: the seller's Velsigil server predates in-app trials; nothing else is affected.
Offline leases
When the product's offline lease hours is above 0, a successful validate() (or start_trial()) returns a signed
lease base64url(JSON).base64url(signature) bound to the product, this device's HWID hash and an
expiry exp = min(now + offlineLeaseHours, licenseExpiresAt) (plus trial: true for a free-trial
license; fields the SDK does not know are ignored). The SDK verifies the lease and then stores it. A validate() answer whose lease is bound to another device or product is
rejected as a whole (invalid_response), not accepted without the lease.
validate_offline()accepts the stored lease only if the signature is valid,typis"lease", product id and HWID hash match, andnow < exp.validate_with_offline_fallback()uses the lease only fornetwork_error. A business failure, rate limiting or an invalid or unsigned response never falls back, because an attacker who can tamper with responses must not be able to switch the app to offline mode.- A signed, definitive denial deletes the stored lease. The set is the same in every Velsigil
SDK (
velsigil_client.LEASE_REVOKING_CODES):invalid_key,license_expired,license_suspended,license_revoked,license_banned,device_revoked,device_verification_failed,device_limit_reached,device_not_activated,device_not_found,blacklisted,product_disabled. A success without a lease (offline leases disabled) and a successfuldeactivate()also clear it. Any other signed failure (product_paused,outdated_version,activation_rate_limited,clock_skew,replay_detected, ...) leaves it alone. - Known limitation: offline validation trusts the local clock (plus any offset learned online). Someone who winds the clock back can extend offline use up to the lease's own expiry. Keep lease hours short.
Device secret persistence
On the first activation the server issues a device secret (activation.deviceSecret). The
SDK stores it in the configured store, keyed by product id, and sends it with every later
validate, deactivate and get_download call. The server keeps only a hash of it.
- Use a persistent store. With the default
MemoryStorethe secret is lost on restart. The server then counts a secret mismatch, and products with strict device binding refuse the device (device_verification_failed) until the customer resets their devices. FileStorewrites atomically with owner-only permissions on POSIX. On Windows,default_store_path()places the file under%LOCALAPPDATA%, which only the user (and administrators) can read. ImplementLicenseStoreto keep it in the OS keychain or your own encrypted settings.- If saving fails, the SDK logs a warning and keeps the state in memory for the rest of the process.
- If loading fails, the SDK logs a warning and keeps working from the last state it read or wrote,
but never treats the failed read as "nothing stored":
start_trial()refuses withstore_unavailable, and an answer is merged into a fresh read of the store. While the store stays unreadable nothing is written except a device secret the server just issued. deactivate()(success) andclear_stored_state()remove the secret.
Updates and downloads
update = client.check_update("1.2.0").update
if update and update.update_available:
link = client.get_download(license_key, update.latest_version)
if link.ok:
path = client.download_to_file(link.download, "downloads/app.zip") # raises DownloadError
get_download returns a signed link that expires within minutes, plus the file's signed
size and sha256. download_to_file streams to a temporary file next to the destination. It
aborts once the download exceeds the signed size, compares the SHA-256 in constant time, and only
then renames the file into place. A tampered or truncated download therefore never appears at
the destination path. Links must be https:// (or local http://); redirects are not followed.
update.mandatory and outdated_version tell you when to block the old version.
Thread safety
A VelsigilClient can be shared between threads:
validate,start_trial,deactivate,get_downloadandclear_stored_stateare serialised per client, because they read and update the device secret and lease. This also guarantees that concurrent first activations don't race for the device secret, and that thealready_licensedcheck ofstart_trialand its request form one step.check_updateandvalidate_offlinerun concurrently.- The clock offset and cached state are protected by a lock.
MemoryStoreandFileStoreare thread-safe;FileStoreobjects on the same path share one lock per process. - Use one
VelsigilClientper product and, ideally, oneFileStoreper file. Separate processes sharing oneFileStorefile are not coordinated: each write replaces the whole file, which holds every product, so a write for one product can drop another product's concurrent update. Let one process own the file, or give each product its own file (FileStore(default_store_path("MyApp", "license-<product>.json"))).
Hardening your integration
Client-side licensing raises the cost of piracy; it can't make it impossible. Plan with that in mind:
- Keep the public key in code as a constant. Don't load it from a config file, an environment variable or the network, or a user can swap in their own key and sign their own "valid" responses. Rotating the signing key in the panel requires shipping a new build.
- Check results in several places, not with one
ifat start-up that a patch can remove. Re-checkresult.okandhas_feature()where features are used, and re-validate periodically (for example every few hours or on important actions). - Server-side enforcement is authoritative. Revocation, device limits, expiry and downloads are enforced by the server. Anything valuable (content, cloud features, updates) should be delivered only after a successful server check, ideally gated by the server.
- Obfuscation helps but is not a security boundary. Python bytecode is easy to inspect, so packers and obfuscators only slow an attacker down.
- Never disable HTTPS (
allow_insecure_http) or certificate verification in production builds. - Treat every
ok=False, and especiallyinvalid_response, as "not licensed". Don't add a "fail open" path. - The SDK never logs license keys, device secrets, lease tokens or download URLs, and the
reprof its objects hides them. Keep it that way in your own logging: show users at most the last 5 characters of a key.
Development and tests
In the SDK repository, from this folder:
python -m pip install "cryptography>=50.0.0"
python -m unittest discover -s tests -t . -v
tests/test_vectors.pyclassifies every envelope, lease and HWID vector in the sharedtest-vectors.json(one folder up) with each installed backend. It also proves that the mock server encodes and signs byte-for-byte like the real server. The private keys of the vectors are published there, soVelsigilClientrefuses their two public keys (ConfigurationError) unlessapi_urlis a loopback host (localhost,127.0.0.1or::1), and the public low-level helpers refuse them on every host; the suite verifies the vectors through the SDK's internal, unguarded path and checks both refusals.tests/test_client.pydrives the client againsttests/mock_server.py, a threaded local HTTP server that signs with the vector key. It covers success, business failure, nonce, product and type mismatch, bad signatures, unsigned success, clock skew with a single retry, device secret persistence and re-sending, 400/403/404/429/500/502/503, redirects, oversized bodies, timeouts, connection refused, offline fallback with a fake clock, deactivation, updates, verified downloads, concurrency and log hygiene.
Set VELSIGIL_TEST_VECTORS to use a vectors file at another location.
Before a release this SDK is also run against a real Velsigil server (activation, device-secret persistence, offline lease, updates, verified download, clock skew, replay, suspension, invalid key, deactivation).
Metadata
Release files for velsigil-client 1.0.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| velsigil_client-1.0.2.tar.gz | 92.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| velsigil_client-1.0.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 143.5 kB
Release files / velsigil_client-1.0.2.tar.gz
| Download URL | velsigil_client-1.0.2.tar.gz |
|---|---|
| Size | 92.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e973bc07e38090cacab8e982f02fde8d9b281ac187671719d9d8426d9a9581b1
|
|
BLAKE2b-256 checksum How to use checksums |
ff29bbf430fe27fc6ef0892b719735e7c783ab189ede37b9df6626bd3ec41be2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency logRelease files / velsigil_client-1.0.2-py3-none-any.whl
| Download URL | velsigil_client-1.0.2-py3-none-any.whl |
|---|---|
| Size | 50.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
09a2c65c0397f93252d15bb0a1d2bab1a4d694f08cc6bb735448d3993b4cae06
|
|
BLAKE2b-256 checksum How to use checksums |
ef20519b950e3efe48a967b28e6c41008a0c46432de0db6853ee66a3f4fef2d0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 8, 2026.
Transparency log