@moonbase.sh/licensing
v3.1.0
Published
Package to add sotftware licensing using Moonbase.sh to your node.js apps
Readme
@moonbase.sh/licensing
Node.js licensing SDK for Moonbase products.
Use it to request activation, validate licenses locally and online, persist licenses, and revoke activations.
Learn more in our official documentation here: https://moonbase.sh/docs/licensing/sdks/node/
Install
pnpm add @moonbase.sh/licensingCreate a licensing client
import { FileLicenseStore, MoonbaseLicensing } from '@moonbase.sh/licensing'
const licensing = new MoonbaseLicensing({
endpoint: 'https://demo.moonbase.sh',
productId: 'demo-app',
publicKey: process.env.MOONBASE_PUBLIC_KEY!,
accountId: process.env.MOONBASE_ACCOUNT_ID,
// Optional analytics reported with every activation and validation.
appVersion: '1.2.3',
metadata: {
Host: 'Console',
User: process.env.USER ?? '',
},
// Optionally adjust the license store with path
// to where the license should be stored, or use
// alternate storage mechanisms to persist the token.
licenseStore: new FileLicenseStore(),
})publicKeyis the Moonbase public key used to verify signed license tokens.FileLicenseStorepersists the license locally (license.mbby default).
Activation flow
const request = await licensing.client.requestActivation()
console.log('Open this URL in a browser:', request.browser)
let license = null
while (!license) {
await new Promise(resolve => setTimeout(resolve, 1000))
license = await licensing.client.getRequestedActivation(request)
}
await licensing.store.storeLocalLicense(license)
console.log('License stored locally')Validate on startup
const localLicense = await licensing.store.loadLocalLicense()
if (localLicense) {
// Validate token signature + device binding locally
const locallyValidated = await licensing.validator.validateLicense(localLicense.token)
// Re-validate against Moonbase (recommended for online activations)
const refreshed = await licensing.client.validateLicense(locallyValidated)
await licensing.store.storeLocalLicense(refreshed)
}Metadata, platform, and app version
The SDK can report analytics with every activation, trial request, and validation. These are configured once on the MoonbaseLicensing config and sent automatically; there's no per-call parameter.
appVersionandplatformare first-class fields and sent as plain query params.metadatais for arbitrary customer-defined keys, sent asmeta[key]=value.platformauto-detects fromprocess.platform(darwin→Mac,win32→Windows,linux→Linux). Set it explicitly to override, or passnullto suppress.- All three are sent on
requestActivation,requestTrial, andvalidateLicense, never onrevokeLicense. metadataaccepts string keys and string values only; empty values are dropped client-side.- Server-side, the metadata on the activation is replaced on every call (not merged) and surfaced in
LicenseActivatedEvent/LicenseValidatedEventwebhooks. - The config object is re-read on every call, so mutating
metadatabetween calls (e.g. to rotate a session id) means the next request picks it up. - Keep payloads small; query-string length limits apply.
Revoke a license activation
const localLicense = await licensing.store.loadLocalLicense()
if (localLicense) {
await licensing.client.revokeLicense(localLicense)
await licensing.store.deleteLocalLicense()
}Offline helpers
Generate a device token to support offline activation workflows:
const deviceToken = await licensing.generateDeviceToken()Read and validate raw license bytes (for example from a file):
const license = await licensing.readRawLicense(rawLicenseBuffer)Device fingerprint
Every license is bound to the machine via a device id, stored in the token's sig claim and
re-checked on each local validation. The default MoonbaseDeviceIdResolver computes it from the
cross-SDK device fingerprint spec (moonbase:fingerprint:v2): a
SHA-256 of stable native hardware identifiers, stamped with the spec version.
mbd2_9f3c… // 'mbd' + version + source tag + '_' + 64 lowercase hex characters
mbd2n_9f3c… // 'n': the opt-in host-name fallback (see below)
mbd2s_9f3c… // 's': an app-scoped id, which only the mobile SDKs produceSources are SMBIOS on Windows, IOPlatformUUID on macOS, and machine-id plus world-readable DMI
on Linux. The algorithm is language-neutral by design: any Moonbase SDK that implements the spec and
passes the shipped fingerprint-vectors.json computes the same id on a
given machine, so a license activated by one validates in the others. Adoption is per-SDK, so check
the version of whichever SDK you are pairing with before relying on it.
This package parses all three forms — including a tag introduced by a newer SDK, which it
compares literally rather than rejecting — but its built-in readers only ever produce mbd2_ or
mbd2n_. Scoped ids come from iOS and Android, where the OS exposes no identifier an unrelated app
can read; collecting them needs platform API no Node.js process can call, so that is the C++/.NET
SDKs' job. A scoped id is stable only within one app scope and must never be compared with one from
another, which is exactly what the s tag exists to announce.
Both files ship inside the package. If the links above do not resolve where you are reading this, find them in
node_modules/@moonbase.sh/licensing/.
The id survives a rename, a locale change, a firmware update, a vCPU resize, and running with or without elevated privileges. The spec's stability contract is the definitive list. Read it before shipping, along with the two Linux exceptions, which exist because every per-unit hardware serial is root-only there and the id is tied to the OS installation rather than the hardware:
- A Linux OS reinstall requires re-activation.
- A Linux VM cloned without clearing
/etc/machine-idkeeps its device id, so a license copied with the disk keeps validating.machine-id(5)requires reusable images to ship that file empty; when they do, clones behave correctly. The SDK cannot detect a badly-prepared image, because the value that would distinguish the instances is root-only.
Because the version is part of the id, a mismatch is diagnosable. validateLicense throws
ErrorType.LicenseDeviceMismatch either way, and the message says which case you are in:
try {
await licensing.validator.validateLicense(token)
}
catch (err) {
if (err.type === ErrorType.LicenseDeviceMismatch)
console.error(err.message) // 'not for this device', plus any version difference
}When there is no hardware identity
The resolver throws InsufficientDeviceIdentityError rather than falling back to something weak, in
two cases:
- Nothing readable. A sandboxed process; a platform the spec defines no parameters for (BSD, and anything unrecognised); or Android, whose parameter this package cannot reach from Node.
- Only model-level values readable. Vendor, product and board names are byte-identical across
every unit of a product line, so fingerprinting them would let those machines validate one
another's licenses. In practice: a Linux install with no
machine-id, or a machine whose SMBIOS carries an unset UUID and a blank or filler baseboard serial, the usual shape of a cloned VM image.
Opt in explicitly if a weaker id beats none. Those ids are stamped mbd2n_ so the server can tell
them apart:
const deviceIdResolver = new MoonbaseDeviceIdResolver({ fallback: 'deviceName' })Diagnostics and parity checks
describeDevice() returns the id, spec version, platform and the names of the parameters that
contributed. It is safe to log or attach to a support ticket, and returns a fresh copy each call so
editing it cannot disturb the binding.
Parameter values are never exposed there, and neither are per-parameter hashes. They are hardware serial numbers, and an unsalted per-value digest is no safer to publish than the value, since low-entropy values such as host names or sequential serials fall to a dictionary. Which parameters contributed is the useful diagnostic; what they read is not.
The device id itself is a one-way hash of all of them together, so it discloses no individual
serial. It is, however, derived identically for every Moonbase-powered product. The material
contains no product- or account-specific input, so the same machine yields the same device id
everywhere, and merchants receive that string through the integration API and webhooks. Treat it as
a stable cross-vendor machine identifier. That is more than machine-id(5) intends, which asks that
the Linux machine id only leave the host through an application-specific keyed derivation. If that
matters for your deployment, supply a custom IDeviceIdResolver that mixes in a key of your own.
The lower-level buildFingerprintMaterial, fingerprintDigest, fingerprintDeviceId and
parseDeviceIdStamp helpers are exported so you can verify cross-SDK parity against the vector
file.
Migrating from v2.x
Device ids computed by v2.x are not compatible with the spec, so by default every device must re-activate once after you upgrade. That is not free: a new device id consumes a fresh activation seat (the old one is not reclaimed) and resets any device-scoped trial. On a license with few seats, a fleet-wide upgrade can exhaust them immediately.
Three options, in increasing order of effort:
1. Let devices re-activate (default). Simplest, and the id is correct from then on. Catch
ErrorType.LicenseDeviceMismatch and call requestActivation(). Best when seats are generous or the
install base is small.
2. Accept the old id while binding the new one (recommended for existing fleets).
MigratingDeviceIdResolver keeps recognising ids this device was previously bound to, without ever
issuing one:
import { LegacyDeviceIdResolver, MigratingDeviceIdResolver, MoonbaseDeviceIdResolver } from '@moonbase.sh/licensing'
const licensing = new MoonbaseLicensing({
// …
deviceIdResolver: new MigratingDeviceIdResolver(
new MoonbaseDeviceIdResolver(), // always what a new activation binds
new LegacyDeviceIdResolver(), // additionally accepted during validation
),
})Existing licenses keep validating untouched, while anything newly activated binds the current fingerprint. The fleet migrates as devices naturally re-activate, with no flag day and no seat churn. The legacy id is computed lazily, only when the fast comparison fails, and then memoized, so apps on the happy path pay nothing. Drop the wrapper in a later release to finish the migration.
3. Stay on the old id. Pin deviceIdResolver: new LegacyDeviceIdResolver(). Nothing changes, but
you keep the old algorithm's defects (the machine name is part of the id, so renaming a computer
invalidates its license) and get no cross-SDK compatibility. Use this only as a short-term hold.
Options 1 and 2 both recompute every accepted id from the machine's own hardware on each call. Nothing about a device binding is ever read from disk, so widening what a validator accepts does not widen what an attacker can assert.
Custom stores and device resolvers
You can inject your own ILicenseStore and IDeviceIdResolver implementations via the
licenseStore and deviceIdResolver configuration options. A custom resolver's id is compared
literally, so it does not need to follow the mbd2_ stamp format.
