Skip to content

Releases: keylight-dev/keylight-js

v0.4.2

Choose a tag to compare

@Halloweedev Halloweedev released this 07 Sep 03:36

Fixed

  • refreshAfterUpgrade could squeeze in one extra poll past its timeout.
    The capped final sleep was followed by a fresh clock read, and a timer that
    fires a hair early relative to performance.now() bought one more
    validate() call. The last wait is now decided up front, so no network call
    starts after timeout. This is the only change since 0.4.1, which was
    tagged but never reached npm; install 0.4.2.

v0.4.1

Choose a tag to compare

@Halloweedev Halloweedev released this 07 Sep 03:31

Fixed

  • refreshAfterUpgrade now honours its AbortSignal and its timeout while
    asleep.
    The wait between polls was a bare setTimeout: the signal was only
    read between iterations, so an abort landed up to a full pollInterval late,
    and a pollInterval longer than what remained of timeout ran the method past
    its own deadline. The sleep now wakes the moment the signal fires, the final
    wait is capped to the time left, and no poll starts once the deadline has
    passed. An already-aborted signal makes zero network calls rather than one
    last validate(). The timer is also unref'd on Node/Bun, so a pending poll
    no longer keeps a one-shot script alive.
  • README documents requireSignedConfig. The 0.4.0 flag was in the
    changelog and the types but not in the README, whose quick start fetches the
    keyset at runtime with fetchKeyset(...) — the one thing you must not rely on
    once the flag is on. The Configuration Reference now covers the flag (off by
    default; enable only for a product with a trial length configured in the
    dashboard; unverified settings are never cached and the seed is kept) and
    states that enabling it means pinning trustedKeys at build time, because a
    keyset fetched over the same connection as the settings can be forged by the
    same attacker. The KeylightOptions doc comment says the same thing instead
    of claiming the SDK never fetches a keyset.

v0.4.0

Choose a tag to compare

@Halloweedev Halloweedev released this 07 Sep 03:31

Added

  • requireSignedConfig — Ed25519 verification of server-owned product
    settings.
    The Keylight worker has signed the trial length and free-tier flag
    on every route that delivers them since 2026-09-06; nothing in this SDK
    checked those signatures. verifyConfig now does, over the payload format
    shared byte-for-byte with the Swift, C#, C++ and Rust SDKs.

    Off by default, and it should stay off unless you know your product is
    signed.
    The worker signs a product's settings only once that product has a
    trial length configured in the dashboard; every other product is served
    unsigned, and enabling this against one of those would reject legitimate
    responses and pin the install to the seed you shipped with.

    When enabled, settings that do not verify are never cached — the SDK keeps
    your seed rather than trusting what the server claimed. The check lives at the
    single point where settings are merged, so no route can write settings around
    it: /config, validate and the keyless beacon all pass through it.

    readConfigFields now carries the four signature fields it previously
    dropped. Without that every route looked permanently unsigned.

    Trust is rooted in the trustedKeys you ship with your app. The SDK does not
    fetch a keyset at runtime, on purpose: keys fetched over the same connection
    that serves the settings would let anyone able to forge one forge the other.
    The trade-off is that rotating to a new key id leaves builds already in the
    wild on their last known settings until they update — they freeze, they do not
    break.

    Verification is pinned by a golden vector captured from the live worker, the
    same one the other SDKs pin.

  • refreshAfterUpgrade(timeout?, pollInterval?, signal?) — brief poll-revalidate
    right after an upgrade.
    Payment webhooks (Stripe/Polar/etc.) can lag a few seconds
    behind the checkout redirect, so a naive single validate() right after upgrade can
    still see the old entitlements. This re-validates every pollInterval (default 2s, in
    milliseconds — this SDK's convention, unlike the Swift SDK's seconds) up to
    timeout (default 30s), and resolves true as soon as the license's entitlements or
    state change from what they were when it was called — including a definitive rejection
    landing mid-poll, since validate() reconciles that into a state change on its own.
    Resolves false on timeout, on an aborted AbortSignal, or immediately (no network
    calls) when there is no stored license. Mirrors the Swift SDK's refreshAfterUpgrade.

v0.3.0

Choose a tag to compare

@Halloweedev Halloweedev released this 07 Sep 03:31

Added

  • The trial length is the server's, not the build's. A tenant could set a
    trial length in the dashboard and nothing happened to their JS app:
    trialDurationDays and freeTierEnabled were read straight off the
    normalized config wherever a trial decision was made, so the value passed to
    new Keylight({...}) was the only one that ever applied. New
    effectiveTrialDurationDays() and effectiveFreeTierEnabled() resolve
    server value → local seed → 0, and checkTrial(), state() and
    productFreeTierEnabled() now read those. The option stays a seed,
    deliberately: a brand-new install genuinely has nothing else, and removing it
    would make first-launch behaviour depend on the network.
  • The settings ride on calls already being made, so launch-time network I/O
    stays at zero. validate covers every licensed install and the keyless beacon
    covers every unlicensed one; both responses now carry trial_duration_days
    and free_tier_enabled, and both are absorbed. fetchConfig() reads the
    dedicated GET /{tenant}/{product}/config route for hosts that want an
    explicit refresh — it is not on the launch path, and a test asserts that
    resolving state never touches it. fetchConfig() never throws.
  • sdk_trial_duration_days on activate and validate — the length the build
    was configured with, not the effective one, since echoing the server's own
    number back diagnoses nothing. It catches the ordinary mistake of a 30-day
    build against a 14-day dashboard setting. Diagnostic only: the server must
    never gate on it, because a patched client sends whatever its author wants.
  • ACCOUNT.PRODUCT_CONFIG ("product_config"), matching the Rust account
    module, and the CachedProductConfig / ProductConfigFields types.

Fixed

  • 0 is a real setting and absence is not zero. The cached pair is stored
    as optional fields and merged one at a time, so a worker that sends neither
    field leaves what the install already learned alone, a server 0 survives a
    relaunch as 0 rather than falling back to the seed, and a server
    free_tier_enabled: false is not mistaken for "never heard".
  • startTrial() stamps the clock even at a zero duration. Once the duration
    is server-owned, 0 is indistinguishable from "the config has not arrived
    yet", so bailing out left no start timestamp for a later-arriving duration to
    measure and the user never got the trial their tenant enabled. The stamp
    grants nothing on its own. An existing stamp is never overwritten, so enabling
    a trial 60 days after an install does not hand it a fresh window.

Notes

  • No breaking change: object literals are name-based, so nothing in
    KeylightOptions moved or was added.
  • 157 → 173 tests.
  • Ports the contract shipped in keylight-cpp 0.2.0/0.2.1; see that repo's
    docs/superpowers/specs/2026-09-05-trial-parity-handoff.md.

v0.2.0

Choose a tag to compare

@Halloweedev Halloweedev released this 03 Sep 16:47

[0.2.0] - 2026-09-04

Added

  • A keyless heartbeat, on by default. reportKeylessState used to go out
    only when the app called it, which in practice is once at startup — so an
    Electron app or a resident Node service reported itself once and then looked
    dead to the dashboard for as long as it ran: last_seen never moved past
    first_seen, and the reported app version froze at whatever shipped that day.

    checkOnLaunch() now starts a cadence (keylessHeartbeatMs, default 6h to
    match the Worker's server-side gate on keyless writes); startKeylessHeartbeat()
    is public for apps that never call checkOnLaunch. Each tick beacons only
    when the device is keyless — a licensed device sends nothing and reports
    liveness through /validate — and the timer keeps running across that
    boundary so a lapsed license resumes on its own.

    Nothing to change in your app. reportKeylessState still debounces to one
    request per 24h, so the cadence costs nothing extra on the wire. The timer is
    unref'd on Node/Bun so a six-hour interval can't keep a one-shot script
    alive, and stopKeylessHeartbeat() disposes it. Pass
    keylessHeartbeatMs: null to opt out.

  • Device dimensions on the telemetry fields: arch and os_version.
    Requests that already carry telemetry (activate, validate, the keyless
    beacon) now also report the CPU architecture as a canonical token (arm64 /
    x86_64; anything else is omitted) and the host OS's release version reduced
    to its dotted-numeric form (a Linux kernel release like 6.8.0-45-generic is
    sent as 6.8.0; on macOS it is the marketing version from sw_vers, e.g.
    26.1, matching what the Swift and Rust SDKs report — not the Darwin kernel
    version, which would place the same macOS release in a different bucket. The
    sw_vers call runs at most once per process, and where it cannot run the
    version is omitted rather than reported in the other vocabulary). Both fields
    are optional on the wire and only sent where a host OS exists to ask: under
    Node, Bun, Electron and Deno — on Deno the macOS version needs --allow-run,
    and is omitted rather than prompting for it. Browser pages and edge isolates
    send neither — there
    is no reliable source there that isn't also a fingerprinting surface, and
    this SDK keeps its stance of not reading one. device_class is never sent;
    the backend derives it from the OS. No app code changes.

  • Coarse device capacity on the same telemetry fields: cpu_cores and
    memory.
    Requests that already carry telemetry report which bucket the
    machine falls in — 1-2, 3-4, 5-8, 9-16, 17+ cores, and <4GB,
    4-8GB, 8-16GB, 16-32GB, 32-64GB, 64GB+ of RAM. The exact core count
    and the exact byte figure never leave the machine: a licensing SDK reporting
    precise hardware specs reads as fingerprinting, so only the bucket is sent.
    Bucket edges match the other SDKs exactly, so one machine population never
    splits across two buckets. Both fields are optional on the wire and only sent
    where a host OS exists to ask (Node, Bun, Electron, Deno). Browser pages and
    edge isolates send neither: navigator.hardwareConcurrency and
    navigator.deviceMemory are documented fingerprinting surfaces and this SDK
    declines to read them, the same stance it takes on arch. No app code
    changes.

v0.1.6

Choose a tag to compare

@github-actions github-actions released this 01 Aug 05:18

[0.1.6] - 2026-08-01

Changed

  • platform now reports the operating system, not the JavaScript runtime.
    It previously sent node / deno / bun / web, which meant every Electron
    app reported node no matter which OS it ran on — so a dashboard could not
    tell a Windows install base from a macOS one, and the breakdown was not
    comparable with the Swift, Rust, C++ and C# SDKs, all of which report the OS.
    It now sends the same canonical tokens they do: macos, windows, linux
    (and an unmapped process.platform value verbatim, e.g. freebsd).

    Where there is genuinely no host OS to report — a page in a browser, an
    isolate on the edge — the runtime token is still the honest answer and is
    kept: web, workers, unknown. The SDK deliberately does not read
    navigator.userAgentData.platform to guess a browser's OS; it is
    Chromium-only and a fingerprinting surface, and this SDK avoids the
    User-Agent for the same reason it avoids it in instance_name.

    No app code changes. If you have been reading the platform breakdown, expect
    node to stop growing and macos / windows / linux to start; historical
    rows keep their old token.

Added

  • sdk field identifying this SDK on activate/validate/keyless calls.
    Sends js. Keylight used to work out which SDK a device ran from the shape
    of its platform token — feasible only while each SDK had its own
    vocabulary, which the change above ends. Exported as SDK_ID. Requires no
    action from you; older SDK versions keep working, and the server falls back to
    the previous inference when the field is absent.

Added

  • activeRevalidate() — forced, 60 s-debounced revalidation for active use.
    Call it when the user brings the app forward (window focus, popover open,
    route change) so a dashboard revoke lands within a minute instead of waiting
    for the next launch. It bypasses refreshIfNeeded's staleness gates
    (5 min / 6 h / 24 h) and shares checkOnLaunch's reconciliation: a definitive
    server rejection downgrades immediately, a network blip never downgrades a
    live session, and it never throws. The debounce is held in memory only, so a
    process restart or page reload always revalidates. Mirrors the Swift SDK's
    activeRevalidate().

Fixed

  • The activeRevalidate() debounce no longer follows the wall clock. It now
    measures elapsed time with performance.now(). Because the debounce
    suppresses revalidation, a system clock moved backwards previously
    suppressed revocation enforcement for the size of the jump — indefinitely, if
    the clock stayed back. Falls back to the previous behaviour only where
    performance is unavailable.

v0.1.5

Choose a tag to compare

@Halloweedev Halloweedev released this 29 Jul 04:47
4ac1583

[0.1.5] - 2026-07-29

Added

  • activeRevalidate() — forced, 60 s-debounced revalidation for active use.
    Call it when the user brings the app forward (window focus, popover open,
    route change) so a dashboard revoke lands within a minute instead of waiting
    for the next launch. It bypasses refreshIfNeeded's staleness gates
    (5 min / 6 h / 24 h) and shares checkOnLaunch's reconciliation: a definitive
    server rejection downgrades immediately, a network blip never downgrades a
    live session, and it never throws. The debounce is held in memory only, so a
    process restart or page reload always revalidates. Mirrors the Swift SDK's
    activeRevalidate().

Fixed

  • The activeRevalidate() debounce no longer follows the wall clock. It now
    measures elapsed time with performance.now(). Because the debounce
    suppresses revalidation, a system clock moved backwards previously
    suppressed revocation enforcement for the size of the jump — indefinitely, if
    the clock stayed back. Falls back to the previous behaviour only where
    performance is unavailable.

v0.1.4

Choose a tag to compare

@Halloweedev Halloweedev released this 17 Jul 04:48

[0.1.4] - 2026-07-17

Added

  • stableDeviceId option for browser-like environments. When no
    OS/hardware machine id is available (browser, Deno, Workers), a host app can
    now supply its own stable identifier — typically a user/account id — as a
    string or (async) function. It is never sent raw: it is hashed with the same
    tenant/product-scoped material as the hardware machine id, and a hardware id
    (Node/Bun) always takes precedence. A null/empty value behaves as unset.
    Note: changing the supplied value changes the device identity server-side.
  • machine_hash on activate and validate. The same cross-SDK device hash
    the keyless beacon sends now accompanies activate and validate, so a
    device that converts from keyless to licensed (or keeps validating) counts
    as one daily-active device instead of two.
  • Durable browser storage fallbacks: IndexedDbStore and CookieStore.
    makeDefaultStore now layers localStorage → fs (Node/Bun) → IndexedDB
    (probed with a real open) → cookies (probed with a write round-trip) →
    memory, so one browser profile keeps one stable free_tier_instance_id
    across page loads even where localStorage is unavailable (sandboxed iframes,
    some privacy modes). Both stores are also exported for explicit use.

Changed

  • reportKeylessState (and reportFreeTier) now return a boolean — true
    when the state is considered reported (HTTP 200, or a still-fresh <24h
    debounce for an unchanged state), false when the send failed. They still
    never throw, and the debounce state is persisted only on a successful 200,
    so a transient failure no longer suppresses re-sends for a day.

v0.1.3

Choose a tag to compare

@github-actions github-actions released this 03 Sep 16:47

[0.1.3] - 2026-07-09

Added

  • Privacy-safe machine identity on keyless beacons. The keyless/free-tier
    heartbeat now sends a one-way machine_hash derived from a stable hardware
    identifier — /etc/machine-id (Linux), IOPlatformUUID (macOS), or
    MachineGuid (Windows) — namespaced to your tenant and product, so the
    dashboard counts one device per physical machine instead of per install (a
    reinstall updates the same free-tier row instead of creating a duplicate).
    Read lazily on Node/Bun only, with no new dependency; omitted in the browser,
    Deno, and Workers, where the SDK keeps using its per-install id. Only the
    SHA-256 hash is transmitted — the raw identifier never leaves the machine.
    Byte-for-byte identical to the Swift and Rust SDKs for the same inputs. Inject
    a custom resolver via the machineId option for tests.

v0.1.1

Choose a tag to compare

@Halloweedev Halloweedev released this 09 Jul 04:08

[0.1.1] - 2026-06-12

  • Documentation and release-tooling fixes.