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

```rust
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):

```rust
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:

```rust
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

```bash
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).

[`cc`]: https://crates.io/crates/cc