Moonbase.Net 4.2.0

dotnet add package Moonbase.Net --version 4.2.0
                    
NuGet\Install-Package Moonbase.Net -Version 4.2.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Moonbase.Net" Version="4.2.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Moonbase.Net" Version="4.2.0" />
                    
Directory.Packages.props
<PackageReference Include="Moonbase.Net" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Moonbase.Net --version 4.2.0
                    
#r "nuget: Moonbase.Net, 4.2.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Moonbase.Net@4.2.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Moonbase.Net&version=4.2.0
                    
Install as a Cake Addin
#tool nuget:?package=Moonbase.Net&version=4.2.0
                    
Install as a Cake Tool

Moonbase.Net Licensing SDK

This package contains API client, persistence layers and device ID resolvers to handle software licensing easily.

It requires you to have an account on https://moonbase.sh, and more documentation on this package can be found in our docs at https://moonbase.sh/docs/licensing/sdks/dotnet/.

Install

dotnet add package Moonbase.Net

Create a licensing client

using Moonbase.Net;
using Moonbase.Net.Storage;

var licensing = LicensingFactory.CreateInstance(opts =>
{
    opts.Endpoint = new Uri("https://demo.moonbase.sh");
    opts.ProductId = "demo-app";
    opts.AccountId = Environment.GetEnvironmentVariable("MOONBASE_ACCOUNT_ID");
    opts.PublicKey = Environment.GetEnvironmentVariable("MOONBASE_PUBLIC_KEY");

    // Optional analytics reported with every activation and validation.
    opts.ApplicationVersion = "1.2.3";
    opts.Metadata = new Dictionary<string, string> { { "Host", "Console" } };

    // Optionally adjust where the license is stored, or swap in another
    // ILicenseStore to persist the token somewhere else entirely.
    opts.LicenseStore = new FileLicenseStore();
});
  • PublicKey is the Moonbase public key used to verify signed license tokens. It is on the product page in your Moonbase account, under the integration guide.
  • FileLicenseStore persists the license locally (license.mb by default).

Validate on startup

Only delete the stored license when Moonbase says the license is gone. Deleting it on any error makes the user re-activate every time a check fails for a reason that has nothing to do with the license, such as opening the app offline.

using System.Net.Http;
using Moonbase.Net;
using Moonbase.Net.Exceptions;

async Task<License?> LicenseOnStartup()
{
    var localLicense = await licensing.Store.LoadLocalLicense();
    if (localLicense == null)
    {
        return null; // show your activation flow
    }

    if (localLicense.ActivationMethod == ActivationMethod.Online)
    {
        try
        {
            // Ask Moonbase first: it is how a revocation reaches the app, and how a token whose
            // expiry has passed is refreshed after a renewal.
            var refreshed = await licensing.Client.ValidateLicense(localLicense);
            await licensing.Store.StoreLocalLicense(refreshed);
            return refreshed;
        }
        catch (Exception exception) when (exception is LicenseRevokedException or LicenseExpiredException)
        {
            // Moonbase says the license can no longer be used on this device.
            await licensing.Store.DeleteLocalLicense();
            return null; // show your activation flow
        }
        catch (Exception exception) when (
            exception is StoreClosedException or HttpRequestException or TaskCanceledException)
        {
            // The store has closed for good (see below), or Moonbase could not be reached right
            // now. Neither says anything against the license, so check it locally instead.
        }
    }

    try
    {
        // Signature, device binding and expiry, which needs no network.
        return await licensing.Validator.ValidateLicense(localLicense.Token);
    }
    catch (Exception exception) when (exception is LicenseInvalidException or LicenseExpiredException)
    {
        // Keep the stored token: only Moonbase can say the license is gone, and the next launch
        // that reaches it can still refresh a token that has expired.
        return null; // show your activation flow
    }
}

To limit how long an app keeps running without reaching Moonbase, compare License.LastVerifiedUtc with a limit of your choosing before returning the locally validated license.

When the store has closed

When a store closes its Moonbase account, buyers keep using what they bought on the devices they have already activated. The next time such a device validates its license online, Moonbase answers with the same license as an offline license, signed with the store's key, and the example above stores it. From then on the license is checked on the device alone and Moonbase is never asked again: a license bought outright keeps working for good, and a subscription until its period ends.

Everything else on a closed store throws StoreClosedException: activating a new device, starting a trial, or validating a trial. When the activation flow throws it, say that the store has closed rather than asking the user to try again.

Do not delete the stored license on StoreClosedException, and do not relax anything because of it. It deliberately does not derive from LicenseInvalidException, so a handler that deletes the stored license on an invalid one leaves it alone; catch it where you catch the failures that mean Moonbase could not be reached, and keep any limit you set on how long the app runs without reaching Moonbase. The answer is not signed, so anyone can fake one; only the signed offline license makes a device independent of Moonbase.

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. As of 4.0.0 the default MoonbaseDeviceIdResolver computes it from the Moonbase 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 iOS and Android produce

Sources are SMBIOS on Windows, IOPlatformUUID on macOS, and machine-id plus world-readable DMI on Linux. The spec is language-neutral by design: any Moonbase SDK that implements it and passes the shipped fingerprint-vectors.json computes the same id on a given machine, so a license activated through one validates through the others.

Adoption is per-SDK rather than automatic, so check that the SDK you are pairing with implements the spec before relying on that.

This package parses all three forms, including a tag introduced by a newer SDK, which it compares literally rather than rejecting. Its built-in readers only ever produce mbd2_ or mbd2n_: a scoped id comes from identifierForVendor on iOS or androidId on Android, and reading either needs UIKit or the Android framework, which none of this package's target frameworks can call. If you can reach those APIs, bridge them, and the resolver stamps the result for you:

opts.DeviceIdResolver = new MoonbaseDeviceIdResolver(new MoonbaseDeviceIdResolverOptions
{
    Platform = FingerprintPlatform.Ios,   // or .Android
    Reader = new MyPlatformIdentityReader(),
});

A bridge owes the spec's collection rules along with the value, since canonicalization is shared with every other SDK and will not reshape a parameter for you: identifierForVendor is UIDevice.identifierForVendor.UUIDString with every - removed and uppercased, and androidId is Settings.Secure.getString(resolver, ANDROID_ID) lowercased. A value in the wrong shape is refused as insufficient identity rather than hashed, so a mistake surfaces immediately instead of as a license that silently fails to validate against every other SDK.

The source is derived from the platform, never chosen by the caller, so a bridged value cannot be mislabelled as a hardware fingerprint. A scoped id is stable only within one app scope and must never be compared with one from another scope, in either direction. That is exactly what the s tag exists to announce; the spec's Scoped identity section is the definitive statement of what it does and does not promise.

Both files ship inside the package. If the links above do not resolve where you are reading this, open the .nupkg or find them in your NuGet package cache under moonbase.net/<version>/.

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 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-id keeps 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.
  • A Linux sandbox that hides /sys/class/dmi/id (some Flatpak and Snap confinements, and containers started without it) changes the device id, because those four model-level fields drop out of the material. Moving an installed app into or out of such a sandbox therefore requires re-activation. This follows from the spec's collection rules rather than from this SDK, so every conforming SDK computes the same changed id; it is not a cross-SDK divergence. If you ship into a confined environment, ship it confined from the start.

Because the version is part of the id, a mismatch is diagnosable:

try
{
    await licensing.Validator.ValidateLicense(localLicense.Token);
}
catch (LicenseDeviceMismatchException exception)
{
    // "License does not match current computer", plus any fingerprint version or
    // identity-kind difference, each of which points at a different remedy. When either
    // id may be app-scoped the lead retreats to "License is bound to a different device
    // id", because a scoped difference does not establish a different device.
    Console.Error.WriteLine(exception.Message);
}

LicenseDeviceMismatchException derives from LicenseInvalidException, so existing handlers keep working. Catch it specifically to offer re-activation rather than discarding the license: a mismatch is not a bad license.

When there is no hardware identity

The resolver throws InsufficientDeviceIdentityException 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 iOS and Android, whose parameters this package cannot reach without a bridged reader.
  • 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.

It is thrown from Validator.ValidateLicense, Client.RequestActivation / RequestTrial / RequestLicense, and GenerateDeviceToken, wherever a device id is required. Nothing is thrown from LicensingFactory.CreateInstance, so you always have a usable client to surface the diagnostic with, and GetDeviceNameAsync() keeps working, so you can still show which machine you are on.

Opt in explicitly if a weaker id beats none. Those ids are stamped mbd2n_ so the server can tell them apart:

opts.DeviceIdResolver = new MoonbaseDeviceIdResolver(DeviceIdFallback.DeviceName);

The fallback is refused on iOS and Android, which throw regardless of this setting. There the host name is not weak identity, it is not identity at all: since iOS 17 gethostname() returns the literal localhost on every device. Falling back would give a whole install base one id, so a single activation would unlock every device. Absence there is transient, so raise the error and retry later.

Diagnostics and parity checks

DescribeDeviceAsync() 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.

// A MigratingDeviceIdResolver is not itself describable; it exposes the resolver it wraps.
var resolver = licensing.DeviceIdResolver is MigratingDeviceIdResolver migrating
    ? migrating.Current
    : licensing.DeviceIdResolver;

if (resolver is IDescribableDeviceIdResolver describable)
{
    var device = await describable.DescribeDeviceAsync();
    Console.WriteLine(
        $"{device.DeviceId} (v{device.Version}, {device.Platform}, " +
        $"from {string.Join(", ", device.ParameterNames)})");
}

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 Fingerprint.BuildMaterial, Fingerprint.Digest, Fingerprint.DeviceId and Fingerprint.ParseStamp helpers are public so you can verify cross-SDK parity against the shipped vector file.

To see what this machine reports, without writing any code:

dotnet run --project tools/Moonbase.DeviceId

That is the same harness the repository's parity CI runs on Linux, macOS and Windows, comparing its output against the published Node SDK on each. The vectors prove the algorithm; that job is what proves the platform reads.

Migrating from 3.x to 4.0

Device ids computed by 3.x and earlier 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 is a new activation, so it consumes a fresh seat (the old one is not reclaimed), and any trial gating that keys on the device signature resets. On a license with few seats, a fleet-wide upgrade can exhaust them immediately, which reaches your customers as "Out of activations".

Three options, in increasing order of effort:

1. Let devices re-activate (default). Simplest, and the id is correct from then on. Catch LicenseDeviceMismatchException and call Client.RequestActivation(). Best when seats are generous or the install base is small. Budget for a support spike.

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:

using Moonbase.Net;
using Moonbase.Net.Device;

var licensing = LicensingFactory.CreateInstance(opts =>
{
    // ...
    opts.DeviceIdResolver = new MigratingDeviceIdResolver(
        new MoonbaseDeviceIdResolver(),          // always what a new activation binds
        new LegacyHardwareDeviceIdResolver());   // 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 applications on the happy path pay nothing. Drop the wrapper in a later release to finish the migration.

The legacy resolvers are marked obsolete, so this produces a compiler warning. That is deliberate: the warning is the reminder to drop the wrapper once the fleet has moved.

If your install base spans several generations, pass them all. The historical resolvers are a params list, tried in order:

opts.DeviceIdResolver = new MigratingDeviceIdResolver(
    new MoonbaseDeviceIdResolver(),
    new LegacyHardwareDeviceIdResolver(),   // Moonbase.Net 2.0 - 3.x
    new LegacyDeviceIdResolver());          // Moonbase.Net 1.x

3. Stay on the old id. Pin opts.DeviceIdResolver = new LegacyHardwareDeviceIdResolver();. Nothing changes, but you keep the old algorithm's defects 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 process. Nothing about a device binding is ever read from disk, so widening what a validator accepts does not widen what an attacker can assert.

Coming from 1.x? LegacyDeviceIdResolver is unchanged in both name and behaviour: it is still the 1.x fingerprint, which included the machine name. The resolver that computes the 2.x and 3.x fingerprint is LegacyHardwareDeviceIdResolver, which was internal before 4.0.0 and is now public so it can be used for migration.

Custom stores and device resolvers

You can inject your own ILicenseStore and IDeviceIdResolver implementations via the LicenseStore and DeviceIdResolver options. A custom resolver's id is compared literally, so it does not need to follow the mbd2_ stamp format.

Implement IMigratingDeviceIdResolver if a custom resolver should also vouch for ids the device was bound to under an earlier scheme; ValidateLicense consults it only after the direct comparison fails.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.2.0 48 10/12/2026
4.1.0 133 8/3/2026
4.0.0 124 7/30/2026
3.0.0 599 5/28/2026
2.0.0 654 4/5/2026
1.1.2 305 4/24/2025
1.1.1 194 1/8/2025
1.1.0 218 1/3/2025
1.0.5 220 10/3/2024
1.0.4 212 10/3/2024
1.0.3 206 10/3/2024
1.0.2 235 7/3/2024
1.0.1 209 6/22/2024
1.0.0 240 1/27/2024

### Minor Changes

- `RequestActivation` gained an overload taking an `ActivationMethod`, so the browser-based activation request can now ask for an offline license with `RequestActivation(ActivationMethod.Offline)`; existing calls are unchanged. The product has to allow offline activations and the license that fulfils the request needs an offline-capable seat, and fulfilling it with a trial still gives an online license. ([#241](https://github.com/Manevann/moonbase/pull/241))
- A store that has closed now throws `StoreClosedException` from every licensing call that reaches Moonbase, where it used to surface as a `LicenseInvalidException` or an `HttpRequestException`, or leave `GetRequestedActivation` polling forever. It is deliberately not a `LicenseInvalidException`: treat it like being offline, keep the stored license and validate it locally, as the README's "Validate on startup" shows, while devices already activated get a signed offline license and keep working. A `catch (LicenseInvalidException)` no longer sees a closed store, so catch `StoreClosedException` where you catch failures to reach Moonbase. ([#239](https://github.com/Manevann/moonbase/pull/239))

### Patch Changes

- `ErrorType` gains `CaptchaRequired`. A store can now require a captcha on sign-up, which an app cannot show, so for such a store `Register` throws a `UserAuthenticationException` saying so: send the customer to the store's sign-up page in a browser instead. ([#193](https://github.com/Manevann/moonbase/pull/193))