Skip to content

Licensing

OpexFlow is commercially licensed: keys are issued from our own license server, activated per device, validated online, with a signed offline grace window, a dev/CI escape hatch, and an R2-backed download worker.

Updated View as Markdown

OpexFlow is commercially licensed. Enforcement is opt-in per build: when license.enabled is set, the connector validates its license key against the OpexFlow license worker on startup (see states). Disabled by default, so dev and CI builds run unenforced.

Configure

license:
  enabled: true
  server_url: "https://flow.opexcg.com"
  key: "${OPEX_LICENSE_KEY}"
  product: "opex-flow"
  offline_grace_days: 7   # default 7; 0 = no offline tolerance
export OPEX_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX
./target/release/opex-flow config.yaml

How it works

The key. It comes from license.key (config file or ${OPEX_LICENSE_KEY}), or you enter it in the dashboard (the License panel) and it is stored in license.json in connector.data_dir (owner-only file mode). If both exist the config value wins; the status says which one is in use (key_source: "config" | "ui"), and only the last four characters of the key are ever shown.

Activation. With no cached activation (or a different key) the connector POSTs { key, device_id, device_name, version } to /license/activate, which binds the key to this host (one seat per device; asking again from the same device returns the same activation) and returns activation_id, plan, expires_at and a signed token. The activation is cached in license.json, so later starts call /license/validate with it instead of using a new seat, and a running connector revalidates every 6 hours. If an admin frees the seat (reason: deactivated) the connector takes a new one automatically when the license has room.

States (GET /api/license/status → state):

State Meaning Data flows
valid The worker confirmed the license. yes
offline_grace The worker is unreachable, but a genuine signed token still covers this device (until offline_valid_until). yes
invalid The worker refused (reason: invalid, revoked, suspended, expired, seats_exhausted, deactivated), or it is unreachable and no token covers us (reason: network). no
unlicensed license.enabled but no key (or the device was deactivated). no
disabled license.enabled: false (or the debug-only skip). yes

invalid / unlicensed keep the API and dashboard up (so a key can be entered) but drop every record at the dispatcher; sources that can redeliver (NATS) keep their messages.

Revoked, expired or suspended is fatal for a config key, as before: the connector shuts down gracefully with a non-zero exit. A UI-entered key that is rejected does not shut the connector down; it stays invalid with data blocked so the key can be replaced. A network error is never fatal while a valid token covers it.

Offline grace relies on a signed token, not on local files. Every successful activate/validate response carries an Ed25519-signed token (binds product, activation and device, with iat/exp, where exp is capped by the license’s own expiry), cached in license.json. If the worker is unreachable, the connector keeps running only while the cached token’s signature verifies against the public key embedded in the binary and the current time is before min(exp, iat + offline_grace_days). Editing the cache file cannot extend the window. The default is 7 days; offline_grace_days: 0 disables offline operation. An explicit refusal is never excused by grace.

Panel / API. GET /api/license/status returns { enabled, state, plan, expires_at, offline_valid_until, activation_id_short, device_id_short, key_last4, key_source, last_check_at, reason?, error? } (times are Unix seconds). POST /api/license/activate { key } (enter or replace the key; refused with 409 while the key is set in the config file), POST /api/license/revalidate (check now) and POST /api/license/deactivate (free this device’s seat and forget the UI key) answer with the new status, or { ok: false, error, reason } (400 bad input, 409 not allowed here, 422 the worker refused, 502 worker unreachable). A failed activation never replaces a working license. Replacing a key frees the old key’s seat on a best-effort basis.

Worker setup. Set the secret LICENSE_SIGNING_KEY (base64 of the PKCS#8 DER Ed25519 private key) and optionally the var LICENSE_TOKEN_TTL_DAYS (default 30, clamped 1-365). Without the secret no token is issued and connectors stay online-only. Upgrade order: deploy the worker with the secret first, then connectors (the first online validation stores a token). To rotate the key, add the new public key to PUBLIC_KEYS in license.rs, ship connectors, then switch the worker secret/kid; drop the old key after old tokens expire.

Dev / CI escape hatch

The bypass is environment-only (never in config) and honored only in debug builds (or builds with the dev-license-skip feature); release binaries ignore it:

OPEX_LICENSE_SKIP=1 ./target/debug/opex-flow config.yaml

Downloads & the worker

Release binaries are served from the download worker (R2-backed): GET /latest?platform=linux-aarch64 redirects to the latest artifact (add &format=json for url, size and SHA-256), and GET /downloads returns the manifest with a per-platform sha256, label and note. Use platform=linux-x86_64-gnu (glibc) when you want Fanuc’s own FOCAS2 library on Linux (every build can use the native FOCAS client instead). Every /releases/* response carries an X-Checksum-Sha256 header. Worker scaffolding lives at apps/landing/; see its README.md for license-server setup (D1 migrations, signing key, Cloudflare Access for /admin) and release publishing.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close