Skip to content

v0.4.0

Choose a tag to compare

@Halloweedev Halloweedev released this 03 Sep 16:45
· 6 commits to main since this release

[0.4.0] - 2026-09-04

Two breaking changes. Both are one-line fixes in your code, but neither is
silent — read them before upgrading.

Changed — BREAKING

  • trial_duration_days now defaults to 0, not 14. Trials are opt-in.
    A product that never calls .trial_duration_days(n) previously granted every
    new install a 14-day full-entitlement trial it never asked for; that
    install now resolves straight to the free tier (or to expired, when the
    product has no free tier).

    If you rely on a trial, you must now say so explicitly:

    KeylightConfig::builder(tenant, product, sdk_key)
        .trial_duration_days(14) // previously implicit — now required
        .build()

    Upgrading without this line silently ends trials for new installs. Existing
    installs already inside a trial window are unaffected: the trial start is
    persisted, and the window is read from it.

  • upgrade_url() now points at the portal's authenticated license route.
    It used to build portal.keylight.dev/p/{tenant}/upgrade/{product}?key=…,
    a retired route — the only public portal page left is /p/{tenant}/claim/ {product}, so customers following the old link landed on a 404 instead of a
    checkout. It now returns:

    https://portal.keylight.dev/t/{tenant}/license/{normalized_key}/upgrade
    

    The customer signs in with a magic link on arrival and upgrades in-portal.
    The key rides in the path (normalized: whitespace and dashes stripped,
    uppercased — matching the portal's normalizedKey), not in a query string,
    and the product id is gone from the URL because the license carries its own
    product server-side. If you display, log, or deep-link this URL, re-check
    those call sites.

Added

  • start_keyless_heartbeat() — a keyless cadence for hosts the Tauri plugin
    doesn't cover.
    report_keyless_state has never had a cadence in this crate,
    so a resident non-Tauri host — a daemon, a service, a desktop app built
    without the plugin — beaconed once at startup and then looked dead to the
    dashboard for as long as it ran: last_seen never moved past first_seen.

    let kl = Arc::new(Keylight::new(cfg)?);
    let _heartbeat = kl.start_keyless_heartbeat(Duration::from_secs(6 * 60 * 60));

    It takes Arc<Self> because a thread cannot outlive a &self borrow, and
    returns an RAII handle: dropping it stops and joins the thread, so the cadence
    can't outlive the scope that asked for it. The thread holds only a Weak, so
    it can never keep the client alive — drop the last Arc and the next tick
    exits. A zero interval is refused rather than spun on.

    Each tick reports only when state() is keyless; a licensed device sends
    nothing and reports liveness through /validate. The thread keeps ticking
    across that boundary, so a lapsed license resumes on its own. The 24h debounce
    inside report_keyless_state is untouched.

    Opt-in by design — unlike the Tauri plugin, the crate does not spawn threads
    behind your back.

  • keyless_state_for(&LicenseState) -> Option<KeylessState> is now public.
    The Tauri plugin had a private copy; the rule now lives in one place and the
    plugin uses it. The match is exhaustive, so adding a LicenseState variant
    fails to compile here rather than silently falling through to "beacon it".

Changed

  • The Tauri plugin's keyless heartbeat is now on by default. A Tauri app is
    typically a tray app that stays resident for days; with the heartbeat off,
    it reported itself once per launch and the dashboard showed a live install as
    last seen on the day it was installed — last_seen never moved past
    first_seen. HeartbeatOptions::default() now has enabled: true.

    The heartbeat no longer bundles license revalidation. It used to call
    refresh_if_needed() on every tick, which would have meant turning on a
    background network call on licensed devices as a side effect of wanting
    liveness. That is now a separate opt-in flag, off by default:

    HeartbeatOptions { revalidate: true, ..Default::default() }

    The default path sends only the anonymous keyless beacon, which the SDK
    already debounces to one request per 24h. Hosts calling init() get the
    cadence with no code change; enabled: false still turns everything off.