Skip to content

Runtime licence check

Downloading LumenFlow requires a licence grant for the private licence registry (see Private distribution). That controls who can get the product at all — it does not, by itself, stop an already installed copy from running after the grant is revoked. The runtime licence check closes that gap: at start, before any gated command runs, the CLI verifies a cached, cryptographically signed licence file before trusting anything in it.

Nobody hand-delivers a licence.lic file. The first gated command with no cached licence file obtains one itself, from the same registry sign-in you already have for the private npm registry — read directly from your npm user configuration, never by spawning npm. Two sign-in forms work:

  • A licence key (issued with your licence): the CLI resolves the licence id and checks out a fresh signed file directly.
  • A user token (issued with your licence): the CLI first lists your own licences with that token, picks the active one for LumenFlow, then checks out the same way.

The obtained file is verified — the exact same signature-then-product check below — before it is ever written to the cache; a bad response is never written. If no sign-in is found, the refusal names the remedy: sign in to the private registry as the install guide describes.

Before checking the signed licence file, a gated command reads the complete LICENSE shipped with the installed CLI package. It resolves that file from the running module’s package location on Windows, macOS, and Linux; your working directory, project LICENSE, and licence configuration cannot supply replacement terms.

The CLI presents the notice and exact installed terms path before accepting. Type accept (or y/yes) at the prompt, or use the existing LUMENFLOW_EULA_ACCEPTED=1 / true environment flow for CI and other non-interactive hosts. The local eula-acceptance.json, beside your licence cache, keeps the acceptance timestamp and SHA-256 hash of every byte of those full terms. Matching records pass silently; a terms-only change or an old short-notice hash requires acceptance again.

Missing or unreadable installed terms refuse before a prompt or record write, even when environment acceptance is set or a prior record exists. Reinstall the CLI with its full LICENSE to repair the installation. Help and lumenflow:doctor remain available: doctor reports accepted, not accepted, or terms unavailable against the same shipped full-terms hash.

This changes no licence restriction. The local record and installed files remain editable by their recipient; the hash does not authenticate legal identity or make local acceptance tamper-resistant.

  1. Read the cached signed licence file from the per-OS location below. Missing → attempt automatic activation (above); still missing after that → refused, with the exact path and the sign-in remedy.
  2. Parse the signed licence file envelope. A malformed file (missing markers, bad base64/JSON, a missing field) is refused.
  3. Verify the Ed25519 signature against the configured public key. No field of the file is read or trusted before this succeeds — an unsigned, wrongly signed, or tampered file is refused at this step, before expiry or entitlement are ever inspected.
  4. Check the licence’s product, expiry, and revocation status.

A valid, fresh file passes with no network call. Only once the cache becomes stale does the CLI check out a fresh signed file (using the licence key already inside the cached file — never re-reading your npm configuration) to renew it — never on every start, and never once the offline grace window has already lapsed (that state is already a final refusal either way).

A network response can only DENY, never GRANT. That call only ever runs from a state the offline check has already allowed; it can mark that state revoked, or supply a fresh file that must independently pass the same signature-then-product verification every cached file does — it cannot grant access beyond what a signature-verified licence file already establishes. No licence key, password, user token, or certificate content ever appears in any output.

A licence does not fail the instant its file goes stale. Two dates come from the signed file itself: the licence’s own expiry, and the cached file’s server-issued freshness window. Once past the earlier of the two, the licence is expiring and later expired-but-still-allowed for a bounded grace period (14 days by default, build-embedded) — long enough to tolerate a machine that is briefly offline. Only after the grace period elapses does the command refuse outright.

Both the grace deadline and the dates it is computed from live inside the signed file, so editing the file cannot push the deadline later — doing so breaks the signature and the file is refused at step 3 above. A small local record of the latest observed time, kept next to the licence cache, also detects a clock moved backwards between runs. Local checks cannot fully protect against a recipient who controls their own machine; see below.

Every refusal states what is wrong and how to fix it — for example, an expired licence past its grace window names where to place a refreshed licence file and to contact the licence issuer. No refusal leaks internals or key material. A broken local licence configuration file, a missing licence, a bad signature, and a lapsed grace window are all reported this way; none of them crash the CLI.

The check runs once, from the CLI’s single shared entry wrapper, so it applies uniformly rather than command-by-command.

CommandBehaviour without a valid licence
--help / -hAlways shown, for every command — unaffected by licence state.
lumenflow:doctorAlways runs (exiting 1, advisory, when no licence is cached). Reports the licence state and cache location so an unlicensed or expired user can self-diagnose without being locked out.
Every other commandRefused with the reason and the remedy; exits non-zero.

Every installed copy of the CLI is gated, including copies in project dependencies and global installs.

Licence:
  ✗ Licence: expired (cache: ~/.config/lumenflow/licence.lic)
    Licence expired and its offline grace window has ended. Connect to the
    network to refresh the licence, or obtain a new licence file and replace
    the cached copy.

The section also validates the registry credential the package manager sends for the licensing registry against the registry itself, because the cached licence can be valid while that credential has expired. It prints the expiry date when known, warns within 14 days (urgently within 3), and fails when the registry reports LICENSE_EXPIRED, LICENSE_SUSPENDED or ACCESS_DENIED. See diagnose and renew an expired registry credential.

Reported states: valid, expiring, expired, revoked, missing, and invalid (a structurally bad, tampered, or wrong-product file). This section is advisory in doctor — the runtime gate above is the actual enforcement point.

PlatformLocation
Windows%APPDATA%\lumenflow\licence.lic
macOS~/Library/Application Support/lumenflow/licence.lic
Linux$XDG_CONFIG_HOME/lumenflow/licence.lic (falls back to ~/.config/lumenflow/licence.lic)

The cache location, the product id, and the pre-expiry warning window are configurable through a JSON config file named by LUMENFLOW_LICENCE_CONFIG_FILE, or a direct override of the licence file path via LUMENFLOW_LICENCE_FILE. The verification public key, the licence service endpoint, and the offline grace window are not configurable. They are fixed, build-embedded values.

Keep the licence file outside your own project’s repository. Every default location above already is. If you override it (via the config file or LUMENFLOW_LICENCE_FILE) to a path inside your repository’s working tree, the licence gate’s own stale-cache refresh rewrites that file in place on later runs — leaving your repository dirty and able to fail any clean-tree check your own workflow runs (a CI step, a release gate). lumenflow:doctor warns when the resolved licence file path is inside the current repository’s working tree.

  • Reading the shipped JavaScript. Unchanged from Private distribution: the licence gate and the EULA are the controls, not the artifact’s readability.
  • Erasing an already-installed copy. Revocation stops a copy from passing this check on its next stale-cache refresh or once its grace window lapses; it does not reach into a running process or delete files.
  • Local state on the recipient’s own machine. A recipient who controls that machine can alter any local record, as with any other local file.

There is no telemetry and no phone-home on every start — the only network calls this check ever makes are the bounded checkout calls described above (first-run activation when no cache exists, or renewal once the cache is already stale).