hymmalm 0.2.0

Licensing client for license-management.com: JWS-signed license verification, trial bootstrap, activation and offline cache. Statically vendors the C core (Tauri-ready).
Documentation

hymmalm — Rust wrapper

Rust binding for license-management-core. All licensing behavior — JWS verification, status rules, machine fingerprint, REST flow, retry policy, clock-tamper resistance — lives in the native core; this crate only marshals. See the repository root README for the architecture.

Usage

use hymmalm::{LicenseClient, LicenseClientOptions, LicenseStatus};

let mut client = LicenseClient::new(LicenseClientOptions {
    product_id: "PRD_01KWWPEPM0N070BDAHJ7G09RGV".into(),
    client_api_key: "PUB_...".into(),                    // never a MST_ key
    jwks_json: std::fs::read_to_string("signingkeys.json")?, // GET /api/signingkeys.json
    license_path: Some(dirs_path.join("license.lic")),
    ..Default::default()
})?;

let status = client.check()?;                            // trial bootstrap on first run
if status == LicenseStatus::InvalidTrial {
    client.activate(&receipt_code_from_user)?;
}

Offline verification only (no network, e.g. checking a provisioned file):

let status = hymmalm::verify(&jws_string, &jwks_json, None, None, None)?;

Errors are hymmalm::LicenseError { code, detail } — code is the native hlm_err (e.g. -7 network failure, -12 invalid API key, -13 trial quota exceeded) and detail carries the server's human-readable refusal reason when it sent one.

Native core — self-contained build

Unlike the Python/.NET wrappers, this crate vendors the C core: the sources are copied under csrc/ (maintained by sync-csrc.sh; CI fails if they drift from the canonical ../../src) and build.rs compiles them with the cc crate, linking statically. There is no shared library to build, ship, or locate — a plain cargo build produces a self-contained binary, and the published crates.io tarball carries everything it needs. You only need a C compiler (cc/clang on Unix, MSVC on Windows; the Windows build links winhttp/bcrypt/ws2_32/advapi32 automatically).

On Linux/macOS the HTTP transport dlopen()s the system libcurl at runtime (any distro flavor works); no curl development package is needed at build time.

After changing the canonical C core, re-run ./sync-csrc.sh and commit the refreshed copies — never edit csrc/ directly.

Publishing

CI publishes to crates.io automatically on v* release tags (the crates job, authenticated by the CARGO_REGISTRY_TOKEN secret). cargo package runs on every CI build to prove the standalone tarball compiles. The integration tests are not packaged (they replay ../../tests/vectors from the repo checkout).

Threading

The native handle must not be used from two threads at once, so LicenseClient is Send but not Sync: move it between threads freely, or wrap it in a Mutex to share it — which is exactly the Tauri pattern below.

Using from Tauri

The crate is Tauri-ready as-is (static native core, no runtime files to bundle). Hold the client in managed state behind a Mutex and expose the operations as commands:

use std::sync::Mutex;
use hymmalm::{LicenseClient, LicenseClientOptions};

struct Licensing(Mutex<LicenseClient>);

#[tauri::command]
fn license_check(state: tauri::State<Licensing>) -> Result<String, String> {
    let mut client = state.0.lock().unwrap();
    client
        .check()
        .map(|s| format!("{s:?}"))
        .map_err(|e| e.to_string()) // e.detail has the server's refusal reason
}

#[tauri::command]
fn license_activate(receipt: String, state: tauri::State<Licensing>) -> Result<String, String> {
    let mut client = state.0.lock().unwrap();
    client
        .activate(&receipt)
        .map(|s| format!("{s:?}"))
        .map_err(|e| e.to_string())
}

fn main() {
    let client = LicenseClient::new(LicenseClientOptions {
        product_id: "PRD_...".into(),
        client_api_key: "PUB_...".into(),
        jwks_json: include_str!("../signingkeys.json").into(),
        license_path: Some(std::env::temp_dir().join("myapp-license.lic")), // pick a real app-data path
        ..Default::default()
    })
    .expect("licensing options");

    tauri::Builder::default()
        .manage(Licensing(Mutex::new(client)))
        .invoke_handler(tauri::generate_handler![license_check, license_activate])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

(tauri is not a dependency of this crate — the snippet goes in your app.)

Tests

cd wrappers/rust
cargo test

tests/vectors.rs replays the same signed vectors as the C suite; tests/flow.rs runs the full client flow (trial → activate → offline cache → deactivate, plus error classification) against a hand-rolled local mock API in a single sequential test. The flow test needs libcurl present at runtime (it is on any normal Linux/macOS install).