@xemahq/license-verifier
v0.1.4
Published
Offline verification of a Xema license — Ed25519 JWS against an embedded public key, lifecycle resolution and entitlement evaluation. Zero dependencies, Node built-ins only, so it runs in the CLI from a bare checkout and in the air-gap installer on a disc
Readme
@xemahq/license-verifier
Offline verification of a Xema license
Overview
This package belongs to Layer 1 — a framework-agnostic runtime SDK with zero
dependencies, Node built-ins only (node:crypto). It verifies a Xema license
offline: no network, no filesystem, no clock during signature verification.
The zero-dependency rule is load-bearing, not stylistic. This code runs inside
the xema CLI, which must work from a bare checkout with nothing installed;
inside the air-gap installer on a machine that has never resolved a DNS name;
and inside platform services. It therefore re-declares the license vocabulary
that @xemahq/license-contracts also declares — the same precedent as
@xemahq/distribution-source-hash — and guards the duplication with a parity
test that loads both packages and fails closed the moment any value or any
field set stops agreeing.
What a license is — and what it is not
A license is long-lived, customer-held and offline-verifiable. It answers "may this customer run this edition at all".
That is a different artifact from the image-delivery entitlement
attestation (ImageDeliveryEntitlementPredicate in
@xemahq/kernel-contracts/distribution, verified by verifyImageDeliveryEntitlement),
which is short-lived, replay-protected by a nonce and an idempotency key,
bound to one release digest, and answers "may these exact bits move to this
customer right now".
The two compose: the license is the credential that authorizes minting a
delivery entitlement. This package neither duplicates nor weakens that
attestation, and the shared | mirror | airgap delivery axis stays where it
already lives (ImageDeliveryMode, CustomerImageEntitlement).
Verifying
import {
EntitlementAction,
EntitlementKind,
evaluateEntitlement,
getProductionLicenseTrustStore,
verifyLicense,
} from '@xemahq/license-verifier';
const verification = verifyLicense(token, {
trustStore: getProductionLicenseTrustStore(),
});
if (!verification.ok) {
throw new Error(`${verification.code}: ${verification.message}`);
}
const decision = evaluateEntitlement(verification, {
kind: EntitlementKind.Biome,
id: 'knowledge-base',
action: EntitlementAction.Install,
});Verification is cryptographic and structural only — it proves the claims
are authentic. It does not mean the license is in force. Expiry,
revocation and suspension are resolved by resolveLicenseState, and whether
anything may actually happen is decided by evaluateEntitlement. Holding a
VerifiedLicense is never an entitlement.
VerifiedLicense is branded with a module-private symbol, so it cannot be
constructed by anything except a successful verification. "Verified" is a
proof, not a naming convention.
Fail-closed behaviour
Every one of the following is a refusal, with a distinct
LicenseVerificationFailureCode so the CLI can render an actionable message:
- a token that is not a compact JWS, has an empty segment, uses non-canonical base64url, or exceeds the size ceiling;
- a header carrying an unknown parameter, an algorithm other than
EdDSA, a media type other thanapplication/vnd.xema.license+jws, or nokid; - a build whose trust store is empty (
TrustStoreEmpty— reported before the signature check, so an unprovisioned build says what is actually wrong); - a
kidthe build does not trust; - a signature that does not verify over the exact transmitted bytes;
- claims with an unknown field, a missing field, a value outside a closed
enum, a duplicate entitlement, a limit that is absent rather than
null, a non-UTC or impossible timestamp, or an expiry before its issuance; - a claims version the build was not compiled to understand;
- an issuer other than the pinned one.
There is no fallback, no coercion, and no path that returns success on doubt.
The expiry ladder
Expiry degrades what a customer may do NEXT; it never stops a running
workload. evaluateEntitlement resolves the whole ladder in one table:
| effective status | stage | RunExisting | Install | Upgrade |
| --- | --- | --- | --- | --- |
| Active | Valid | yes | yes | yes |
| Expired | Grace (30 days) | yes | yes | yes |
| Expired | Restricted | yes | no | no |
| Suspended | any | yes | no | no |
| Revoked | any | no | no | no |
Revoked is the single, deliberate exception to "never stop a workload",
because a revocation is for cause rather than for a lapsed renewal.
A clock rollback — the system clock behind the highest instant this
installation has seen — is reported on LicenseState.clockRollbackDetected
and changes no decision. Disconnected hardware genuinely drifts backwards, and
punishing a customer automatically for a dead RTC battery would make the
licensing plane the cause of the outage. Warn loudly; do not deny.
Clock skew is tolerated up to one minute by default and five minutes at most. A tolerance is a concession to drift, not a dial for extending a license.
Key management
- The private signing key never exists inside any repository and is never
an input to anything in this package.
xema-license-keygenrefuses to write a key inside a git working tree, at any depth. - Only the public verification key is embedded, in
src/embedded/license-material.generated.ts. kidis derived, never assigned:base64url(sha256(SPKI DER)). The same key produces the same id in the ceremony, in the trust store and in every issued license, with no bookkeeping in between.- Rotation is the trust store: it holds several keys at once, so a new key
becomes trusted by shipping a build that contains it and stops being trusted
by shipping a build that does not. Both keys coexist across the rollover.
An unknown
kidis always a refusal — there is no discovery. - Signing goes through the
LicenseSignerinterface, so moving from a local key to Vault Transit or a cloud KMS is a provider swap.
The checked-in material is provisioned
The signing ceremony has run: this package carries a real Ed25519 verification
key and a genuinely signed open-source license, and
isLicenseMaterialProvisioned() returns true.
Before the ceremony it carried a placeholder — an empty trust store and an
unsigned license, so every verification failed with TrustStoreEmpty,
including the embedded license itself. That is the correct unprovisioned
state: a placeholder that verified without a real signature would mean the
licensing plane accepts forgeries in exactly the builds nobody remembered to
provision.
Two independent assertions keep it that way, and they check different things:
src/lib/embedded-license.test.tsproves the checked-in source is provisioned and that its open-source license actually verifies.pnpm check:shipped-materialproves the builtdist/— the bytes npm uploads — is, and runs in bothci.yamlandrelease-npmjs.yml.
The second is not redundant. Version 0.1.0 was published from a build whose
dist/ still held the placeholder while the source was fine; the unit test
passed and every consumer that resolved 0.1.0 refused every licensing
decision with TrustStoreEmpty. The release workflow runs no tests, so only a
gate on the artifact catches that.
The signing ceremony
Run once, offline, on a machine that is not the build machine.
# 1. Generate the Ed25519 signing key OUTSIDE any repository.
# The command refuses to write into a git working tree and refuses to
# overwrite an existing key.
xema-license-keygen --out ~/.xema-license/signing-key.pem
# 2. Mint the open-source license and embed the PUBLIC material.
# Run from this package's root; it self-verifies the minted license through
# the ordinary verification path before writing anything.
cd packages/kernel/license-verifier
xema-license-mint-oss \
--key ~/.xema-license/signing-key.pem \
--issued-at 2026-07-31T00:00:00.000Z \
--write-embedded
# 3. Rebuild, run the tests (the placeholder assertions in
# `embedded-license.test.ts` must be updated in the same change to assert
# that the embedded license now VERIFIES), and commit only
# src/embedded/license-material.generated.ts.Then store the private key SOPS-encrypted in xema-deploy, mounted to
license-api only. Never commit it, never paste it into an issue, and
never place it in a directory that a repository might later be initialised in.
A lost signing key cannot be recovered; a leaked one invalidates every license
until a rotation build reaches every customer.
Signing
import { LocalKeyLicenseSigner, signLicense } from '@xemahq/license-verifier/signing';The signing half sits behind its own subpath so importing a verifier never
pulls in a minter. It lives in this package because signing and verifying must
share ONE definition of the signing input and ONE claim parser — a second
implementation in the issuing service is how an issuer starts minting licenses
its own verifier rejects. signLicense validates claims through that parser
before signing, so a malformed license cannot be produced at all.
Related packages
| Package | Role |
| --- | --- |
| @xemahq/license-contracts | The Layer 0 claim shape and closed enums. |
| @xemahq/kernel-contracts/distribution | The complementary, short-lived image-delivery entitlement attestation. |
License
Apache-2.0 © Xema — xema.dev
