Node.js SDK
This guide will get you all set up with our Node.js SDK to integrate the licensing part of Moonbase into your node-based apps.
Using this SDK is great if you are building Electron apps or other app running on devices directly. It is not suitable for web-apps, since we need to fingerprint the underlying device, and we are currently relying on system information to do that.
Check out our simple Electron sample app for an example of how this integration can work:
Sample Electron app that integrates Moonbase for licensing
Getting started
Start by adding the npm package to your project:
npm install @moonbase.sh/licensing --save
If you haven't already, create a product in the Moonbase app, and check out the Implementation guide to get the relevant cryptography keys, endpoints and configuration. An example configuration could look like this:
import { FileLicenseStore, MoonbaseLicensing } from "@moonbase.sh/licensing"
const licensing = new MoonbaseLicensing({
productId: 'demo-app',
endpoint: 'https://demo.moonbase.sh',
publicKey: `-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAutOqeUiPMgYjAwQ53CyKhJSqojr2bejce0CshQi9Hd8mNZbkoROx
oS56eIzehFSlX4YwHnF47AR1+fPOe7Q33Cgzd6d9xqksiMH7sWK2mADIlB66vZdW
uk3Me0UMB22Biy1RQbSRMivu79MxCofsympoL/5CFjJLd1u37kxjuRWVLjJS84Rr
3L2W7R7Exnno/giC+L/Dv711mjgstmtlAQm5ZINvFvoLA1eFTDs6nlCs3dpJSiq3
fsBUMT9FtudzS5As54jeT/8MB66fJJ0A1LQ/v5CW8ACQYseFSIoOKErD3xU7QLIJ
ERUn++6CVMPvZo67jVbTY+GCXYfW4gGVZQIDAQAB
-----END RSA PUBLIC KEY-----`,
// 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(),
})
This snippet sets up an instance of the licensing client where you will find all the features described below.
The licensing instance will come with four main features:
ILicenseStorethat handles license token persistence, either to file or in-memoryILicenseClientwhich is a HTTP client built around the Moonbase licensing APIILicenseValidatorthat can validate license tokens with the given public keyIDeviceIdResolverwhich by default generates unique device fingerprints using a number of factors
Like described in activation flows, multiple activation flows are possible through Moonbase, and the Node.js SDK currently supports browser based activations and offline activations only. Let's take a look at how each would be implemented using the SDK.
Browser based activations
Using browser based activation is the recommended flow, and the easiest to get going with. To start, request an activation using the SDK:
const activationRequest = await licensing.client.requestActivation()
This activationRequest contains a browser URL that the customer can use to fulfill the request, so let's open a browser:
import open from 'open'
open(activationRequest.browser)
While the customer is activating the license or requesting a trial, we can poll for completion:
let license: License | null = null
do {
await new Promise((resolve) => setTimeout(() => resolve(void 0), 5000))
license = await licensing.client.getRequestedActivation(activationRequest)
} while (license == null)
As soon as the customer has fulfilled the request, a License will be ready for us.
The Moonbase SDK will ensure any License coming through the API client contains a valid signature, and matches the current device automatically.
Keep in mind that this license activation might be a full license, or a time-scoped trial.
To make sure the customer doesn't have to keep doing this, it's best to persist the license to disk:
await licensing.store.storeLocalLicense(license)
That way you can easily add a check when your app starts, to see if you can skip product activation entirely:
const localLicense = await licensing.store.loadLocalLicense();
const validatedLicense = await licensing.client.validateLicense(localLicense);
// License has been re-validated, store updated license for next check
await licensing.store.storeLocalLicense(validatedLicense);
Our Electron app sample has a more complete startup guard that might be helpful: withLicensing
Offline activations
You might have customers that need to be able to activate devices without connection to the internet. To facilitate this, Moonbase signs all license tokens we issue with the unique signature of the device being activated. Since offline devices cannot transmit this device signature over the internet, the app needs to generate a device token. This device token contains the necessary information to generate a valid license token for offline activations, and can easily be exchanged for a license by the customer in the customer portal.
To start, generate a device token:
import fs from 'node:fs/promises';
import path from 'node:path';
const bytes = await licensing.generateDeviceToken()
const tokenPath = path.join(path.resolve(), 'device.dt')
await fs.writeFile(tokenPath, bytes)
The snippet above generates the token, and then writes the result to a device.dt file.
You can change the file name, but keep the extension to ensure compatability with the customer portal interface.
It's up to you to provide the necessary instructions to the customer for how to upload the device token to your customer portal.
What customers receive when they upload the device token in the customer portal is a license.mb file.
This file contains a valid offline activated license token that you can read in using the SDK:
const licenseBytes = await fs.readFile(path);
const license = await licensing.readRawLicense(licenseBytes);
// License has been read, store license for next check
await licensing.store.storeLocalLicense(validatedLicense);
Keep in mind that this license will have a ActivationMethod of Offline, which means trying to re-validate the license is not necessary.
Add-ons
An add-on (a product with an ID like demo-app.preset-pack) can be included in its main product's license, with Include in the main product's license in the add-on's licensing settings.
When a device activates or validates the main product, it then takes a seat of each included add-on the customer owns.
From version 3.3.0 of the SDK, the license lists them in subProducts, so one license answers for both:
import { hasSubProduct } from '@moonbase.sh/licensing'
if (hasSubProduct(license, 'preset-pack')) {
// unlock the preset pack add-on
}
hasSubProduct takes the add-on's own part of its ID or the whole ID, and is true while the license lists the add-on as active and it hasn't expired.
An add-on the customer owns, but with every seat taken on other devices, is listed with active: false.
An add-on bought after the device activated shows up at the next validateLicense.
If the customer revokes an add-on's seat on this device, validating doesn't take it back, and the license lists it with revoked: true.
To take a seat again, or to activate add-ons only when the customer asks for them after setting autoActivateSubProducts: false in the configuration, call:
const license = await licensing.client.activateSubProduct(localLicense, 'preset-pack')
await licensing.store.storeLocalLicense(license)
It throws a MoonbaseError with ErrorType.NoEligibleLicense when the customer doesn't own the add-on or has no free seat of it.
To let a customer try an add-on that offers trials, start its trial from the main license. This needs an online license of the main product, not a trial, and each device gets one trial of each add-on:
const license = await licensing.client.requestSubProductTrial(localLicense, 'preset-pack')
await licensing.store.storeLocalLicense(license)
To build an in-app store or upgrade screen, list the add-ons the customer can do something with, each with its prices, whether to unlock it, its trial and a buy link:
const { currency, subProducts } = await licensing.client.listSubProducts(localLicense)
for (const addOn of subProducts) {
const price = addOn.product.defaultVariation?.price[currency]
if (addOn.active) {
// owned, or trialing, on this device
}
else if (addOn.trial?.status === 'Available') {
// offer "Try it", which calls requestSubProductTrial
}
else if (addOn.buyUrl) {
// offer "Buy" for `price`, opening addOn.buyUrl in the browser
}
}
ownedSubProductIds still lists every add-on the customer owns, included in the license or not, without saying whether it may be used on this device.
Licenses that cover some versions
Licenses that cover only some releases are in beta, and are switched on account by account. Reach out to support if you'd like to sell them.
A license bought outright can cover only some versions of your app, such as 12 months of updates or every version up to 2.x.
The license never expires because of it: it just stops covering newer versions.
From version 3.3.0 of the SDK, it says what it covers in license.releaseAccess, which is absent when it covers every version:
updatesUntil: releases published until this date are covered, and every version while it hasn't passed.upToMajor: every version whose major is at most this is covered.upToVersion: once updates have ended, the last version published before they did, covered along with every older one.
A license with both limits covers a version that either of them covers.
Set appVersion in the configuration for the version to be checked:
const licensing = new MoonbaseLicensing({
productId: 'demo-app',
endpoint: 'https://demo.moonbase.sh',
publicKey: '...',
appVersion: '2.4.1',
})
Activating or validating a license that doesn't cover appVersion then throws a ReleaseNotCoveredError, and so does licensing.validator.validateLicense on the device, offline licenses included.
Set enforceReleaseAccess: false to turn off the check on the device; Moonbase still checks online activations and validations.
Don't delete the stored license when this happens, since it still covers the versions it did.
When validating online, err.license holds a fresh copy of it to store instead.
Tell the customer what their license covers, and offer the upgrade:
import { ReleaseNotCoveredError } from '@moonbase.sh/licensing'
try {
const validatedLicense = await licensing.client.validateLicense(localLicense)
await licensing.store.storeLocalLicense(validatedLicense)
}
catch (err) {
if (err instanceof ReleaseNotCoveredError) {
if (err.license)
await licensing.store.storeLocalLicense(err.license)
// err.releaseAccess is what the license covers, and err.upgradeUrl where the
// customer buys the upgrade (absent when the check on the device refused it)
showUpgradeDialog(err.releaseAccess, err.upgradeUrl)
}
}
To check another version, such as an update before offering it for download, use coversVersion(license, version), which applies the same rule as Moonbase.