@leumas/licensing
Leumas licensing — Ed25519-signed OFFLINE entitlement tokens (LEV1.<payload>.<sig>), license-key mint/hash, the platform issuer service + routers, and the appliance client. The 'buy on leumas.tech →...
@leumas/licensing
Ed25519-signed offline entitlement tokens — the licensing seam between the Leumas-hosted platform and self-hosted Studio appliances (the EXE). Lands the unit deferred in ops/migration/imperium-hosting.json ("offline signed entitlement tokens").
Built (M2 groundwork, verified)
src/token.js—signEntitlementToken/verifyEntitlementToken/needsRefresh.
Format LEV1.<b64url payload>.<b64url sig>; node:crypto only. Payload carries entitlement_policies-shaped docs + a tenant features map + iat/nbf/exp/refreshAfter.
src/keygen.js—LEUMAS-XXXX-XXXX-XXXX-XXXXmint (Crockford base32), peppered sha256 storage
hash (apikeys discipline), keyLast4.
scripts/generate-keypair.mjs <kid>— public key →keys/public-keys.json(committed, ships in
the EXE); private key → LEUMAS_LICENSE_SIGNING_KEY on the platform ONLY.
To build (M2 remainder — see ops/migration/studio-desktop-licensing.json)
src/service.js— platform issuer: issue-on-membership (subscribe to auth's membership events),
activate (key → activation row + signed token), refresh (heartbeat + revocation check), deactivate, seat/activation limits. Tables: licenses + license_activations in @leumas/schemas (real Drizzle tables — money-adjacent).
src/client.js— appliance: persistslicense.jsonunder@leumas/runtime-pathsdataRoot,
daily refresh with offline grace (token stays valid until exp), degrades to a banner — NEVER bricks local data; lastSeenWallClock clock-tamper basic; onChange → entitlements.refresh().
routers/platform.router.js(/licensing/*, platform role only) +
routers/appliance.router.js (replaces the /api/license stub in products/leumas-api/src/routes/activation.js behind the SAME route shapes).
Desktop sign-in — the key-free door
POST /licensing/desktop/signin (platform) is what lets the Studio EXE's first run ask for a leumas.tech email and password instead of a licence key.
EXE first run ──-> POST /api/license/signin (appliance router)
└-> client.signIn()
└-> POST /licensing/desktop/signin (platform router)
├ verifyCredentials() ← injected usersService.login
└ service.signInDesktop() → {token, license, licenseKey}
<-─ appliance: provisionFederatedUser() + startSession() → session cookie
Three rules hold it together:
- The password is checked by the platform, and never travels further.
@leumas/licensinghas
no @leumas/auth; the host injects verifyCredentials. Every failure — wrong password, unknown address, deactivated account — comes back as one indistinguishable 401, so the endpoint is not an account-enumeration oracle. It sits behind the same 10-per-15-min limiter as key activation.
- One licence per DEVICE, key rotated on every sign-in. The appliance needs a plaintext key to
run its heartbeat and the platform stores only a hash, so a device signing in again is handed a NEW key rather than one nobody can recover. Rotation touches only that device's row; a shared row with a rotating key would cut off every other install the customer owns.
- After the save, the state on disk is identical to a key activation. Same
licenseKey, same
instanceId, same token — so refresh, deactivate, revocation, offline grace and the relay have exactly one code path, not two.
Sign-in is offered only when the appliance has a central server (LEUMAS_LICENSE_SERVER). Without one there is no account to check, so /api/license/signin answers 501 and GET /api/license/status reports signIn: false — which is what the first-run wizard reads to choose its flow. It never guesses.
The trust split (do not change casually)
The policy ceiling is signed by Leumas (the appliance's entitlements engine loads policies from the token, never from its own DB); membership assignment stays local (the customer's admin grants plans to their team via the existing offline grant path in @leumas/auth).
Desktop sign-in does not bend that split. It proves WHO centrally and mirrors that identity into a local row (provisionFederatedUser, admin only if it is the first account); what that person may do on the box is still local RBAC under a Leumas-signed ceiling.
What it exports
26 exports: 2 components, 0 hooks, 24 helpers. Import from @leumas/licensing.
RELAY_COLLECTIONRELEASE_CHANNELScreateInstanceAuthcreateLicenseClientcreateLicenseStatusRoutercreateLicensingRoutercreateLicensingServicecreateRelaycreateStudioLinkcreateUpdatesRouterhashLicenseKeyhashLicenseKeykeyLast4keyLast4loadPublicKeysmintLicenseKeymintLicenseKeyneedsRefreshneedsRefreshnormalizeLicenseKeynormalizeLicenseKeyresolveInstancePublicKeyssignEntitlementTokensignEntitlementTokenverifyEntitlementTokenverifyEntitlementToken