Skip to content

Moonbase-sh/moonbase-cpp

Repository files navigation

Moonbase C++ Activation SDK

Header-only C++17 SDK for Moonbase license activation. It supports activation requests, polling for fulfilled activations, local RS256 JWT validation, overridable device fingerprinting, and overridable license storage.

Requirements

  • CMake 3.20 or newer
  • A C++17 compiler
  • Windows, macOS, or Linux (the default fingerprint provider has native implementations for each)
  • CURL::libcurl and OpenSSL (OpenSSL::SSL, OpenSSL::Crypto) — must be findable on the system (e.g. via your distro, Homebrew, or vcpkg)
  • nlohmann_json 3.11+ — used if find_package(nlohmann_json) succeeds; otherwise it is fetched automatically at build time from the upstream release tarball

The installed package config calls find_dependency() for CURL, OpenSSL, and nlohmann_json, so a consuming project does not need to repeat those find_package calls itself — but the libraries must be available when find_package(moonbase_cpp) is invoked.

Installation

Install from source

Clone the repository (or download a release tarball at https://github.com/Moonbase-sh/moonbase-cpp/archive/refs/tags/v<version>.tar.gz), then configure, build, and install:

cmake -B build -DMOONBASE_BUILD_TESTS=OFF -DMOONBASE_BUILD_EXAMPLES=OFF
cmake --build build
cmake --install build --prefix /your/prefix

FetchContent

To pull the SDK into your own CMake build without a separate install step:

include(FetchContent)
FetchContent_Declare(moonbase_cpp
    GIT_REPOSITORY https://github.com/Moonbase-sh/moonbase-cpp.git
    GIT_TAG v3.1.0)
set(MOONBASE_BUILD_TESTS OFF)
set(MOONBASE_BUILD_EXAMPLES OFF)
FetchContent_MakeAvailable(moonbase_cpp)

target_link_libraries(your_app PRIVATE moonbase::licensing)

add_subdirectory() works the same way if you vendor the source tree into your repo.

CMake

find_package(moonbase_cpp REQUIRED)

target_link_libraries(your_app PRIVATE moonbase::licensing)

The package exports the moonbase::licensing interface target, which propagates the include directory along with CURL::libcurl, OpenSSL::SSL, OpenSSL::Crypto, and nlohmann_json::nlohmann_json as transitive dependencies.

The build provides three options, all useful when consuming the SDK as a subproject:

Option Default Purpose
MOONBASE_BUILD_TESTS ON for the top-level project, OFF as a subproject Build the doctest-based unit and live tests.
MOONBASE_BUILD_EXAMPLES ON for the top-level project, OFF as a subproject Build the standalone activation example under examples/.
MOONBASE_BUILD_JUCE_EXAMPLE OFF Fetch JUCE and build the JUCE bridge example (see below).

Override MOONBASE_BUILD_TESTS and MOONBASE_BUILD_EXAMPLES explicitly when you want a subproject integration to build SDK artifacts too.

Basic Usage

#include <moonbase/moonbase.hpp>

moonbase::licensing_options options;
options.endpoint = "https://demo.moonbase.sh";
options.product_id = "demo-app";
options.public_key = public_key_pem;
options.account_id = "tenant-id"; // optional issuer check
options.http_connect_timeout = std::chrono::seconds(10);
options.http_request_timeout = std::chrono::seconds(30);

moonbase::licensing licensing(options);

auto request = licensing.request_activation();
std::cout << "Open: " << request.browser_url << "\n";

std::optional<moonbase::license> license;
while (!license) {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    license = licensing.get_requested_activation(request);
}

licensing.store().store_local_license(*license);

On startup, validate the stored token. validate_token_online runs the local checks (signature, device fingerprint, expiry) and then re-validates against the Moonbase API when needed:

if (auto local = licensing.store().load_local_license()) {
    auto validated = licensing.validate_token_online(local->token);
    licensing.store().store_local_license(validated); // persist refreshed token
}

Two licensing_options knobs control how often the API is contacted and how much offline tolerance is allowed:

  • online_validation_min_interval (default 5 minutes) — if the local validated_at is newer than this, the API call is skipped. Makes the method cheap to call frequently (e.g. on every plugin instantiation).
  • online_validation_grace_period (default 7 days) — maximum age the local token may reach without a successful online check. Within grace, transient API failures (network down, 5xx, etc.) fall back to the local result. Beyond grace, the failure is propagated.

Definitive server rejections (license_invalid_error, license_expired_error) always propagate regardless of grace.

Offline-activated tokens (activation_method::offline) are validated locally even when calling validate_token_online — the SDK never contacts the API for them. Use validate_token_local directly when you want the local-only check explicitly.

Revoking an Activation

To free up the activation seat for the current device — typically wired to a "Deactivate" or "Sign out" button — call revoke_activation with the JWT:

if (auto local = licensing.store().load_local_license()) {
    licensing.revoke_activation(local->token); // server-side revoke + clears local store
}

On success the SDK both tells the server to release the seat and deletes the matching license from the local store. Revoke is only meaningful for online-activated paid licenses; calling it for offline or trial tokens raises operation_not_supported_error without contacting the API. Server rejections (license_invalid_error) and transport failures (api_error) propagate the same way they do for validate_token_online, but with no grace-period fallback — revoke is a one-shot operation.

Offline Activation

For machines without internet access, Moonbase supports a file-based flow: the app emits a device token ("machine file"), the user exchanges it for a license token on the Moonbase activation page, and the app reads that token back in. No network is involved on the device.

  1. Generate the device token and write it to a file (conventionally .dt):

    const auto device_token = licensing.generate_device_token();
    std::ofstream("device-token.dt") << device_token;
  2. The user uploads device-token.dt and receives a license token file (the raw JWT, conventionally license.mb) in return. They can do this through any of:

    • Moonbase's hosted portalhttps://<your-tenant>.moonbase.sh/activate.
    • The embedded storefront on your own site — trigger the activate_product intent (Moonbase.activate_product()), which prompts for the device token and hands back the license token file. The deviceTokenFileExtension (default .dt) and licenseTokenFileName (default license-file.mb) config options control the file types involved.
    • Your own custom flow — drive the exchange yourself with the Moonbase APIs and SDKs (the /api/customer/inventory/activate endpoint).
  3. Read the downloaded token back in, validate it locally, and persist it:

    std::ifstream file("license.mb");
    const std::string token((std::istreambuf_iterator<char>(file)),
                            std::istreambuf_iterator<char>());
    
    auto license = licensing.read_offline_license(token); // local validation only
    licensing.store().store_local_license(license);

read_offline_license runs the same local checks as validate_token_local (signature, audience, issuer, device fingerprint, expiry) and additionally requires the token to have been issued via offline activation, throwing license_invalid_error otherwise. On startup, validate the stored token with validate_token_local — offline tokens are never re-validated against the API and cannot be revoked; they stay valid until the machine's device fingerprint changes.

Custom Fingerprinting and Storage

class my_fingerprint final : public moonbase::fingerprint_provider {
public:
    std::string device_name() const override { return "Studio Mac"; }
    std::string device_id() const override { return "stable-device-id"; }
};

auto store = std::make_shared<moonbase::file_license_store>("licenses/license.mb");
auto fingerprint = std::make_shared<my_fingerprint>();
moonbase::licensing licensing(options, store, fingerprint);

The default store is in-memory. file_license_store persists a JSON representation of the validated license.

The default fingerprint provider builds a stable, native hardware fingerprint from platform identity parameters such as SMBIOS fields on Windows, IOPlatformUUID on macOS, and board/BIOS/CPU fields on Linux. Use a custom fingerprint_provider when you need an exact legacy fingerprint or any other application-specific device ID. If you include narrow SDK headers instead of <moonbase/moonbase.hpp>, include <moonbase/default_fingerprint.hpp> for the native provider and <moonbase/http_curl.hpp> for the default CURL transport.

JUCE Plugins

For JUCE-based plugins and applications, the SDK ships a drop-in bridge (docs/juce.md) that wires Moonbase activation into juce::OnlineUnlockStatus, sources the device fingerprint from JUCE's SystemStats helpers, and populates activation metadata with host/system context (DAW, plugin format, OS, CPU, JUCE version). The bridge header lives at examples/juce/MoonbaseJuceBridge.h and is copy-pasteable into any JUCE project.

Reference implementation: HALO

HALO by Corino is a JUCE 8 standalone GUI application built specifically as a reference implementation of this SDK. It's a saturator-styled app that doesn't actually process audio — the entire point is the license-gate workflow around it:

  • Startup runs a synchronous local JWT check, then re-validates against the Moonbase API on a background thread via tryLoadStoredLicenseAsync.
  • Browser activation handshake with 1-second juce::Timer polling.
  • "Sign out" menu item wired to revokeActivationAsync with a graceful NotRevokable fallback to a local-only forget.
  • file_license_store persisted under the platform's per-user app data directory.
  • GitHub Actions release pipeline that builds on macOS + Windows and publishes binaries straight to a Moonbase tenant.

HALO vendors the bridge header verbatim from this repo and consumes moonbase::licensing via FetchContent — see src/license/HaloLicenseBridge.cpp and its CMakeLists.txt for the full wiring.

Building the in-repo example

A smaller standalone example also ships in this repository:

cmake -B build -DMOONBASE_BUILD_JUCE_EXAMPLE=ON
cmake --build build --target MoonbaseJuceExample

The flag is opt-in — JUCE is fetched and compiled only when it's set.

Live Tests

Unit tests do not hit the network. Live API tests are opt-in:

scripts/test.sh
scripts/test.sh --live

Defaults target the demo setup used by the Node SDK:

  • MOONBASE_CPP_ENDPOINT, default https://demo.moonbase.sh
  • MOONBASE_CPP_PRODUCT_ID, default demo-app
  • MOONBASE_CPP_PUBLIC_KEY, default demo public key
  • MOONBASE_CPP_ACCOUNT_ID, optional issuer check

Live tests create a unique activation request and try to fulfill it through the anonymous trial endpoint.

Releases

Releases are fully automated by semantic-release running on every push to main. The next version is determined by Conventional Commits:

  • fix: ... → patch (e.g. 0.1.00.1.1)
  • feat: ... → minor (e.g. 0.1.00.2.0)
  • feat!: ... or any commit with a BREAKING CHANGE: footer → major

Pull requests must be merged with squash merging, and the PR title must follow Conventional Commits — that title becomes the squash commit on main and is what semantic-release reads. The PR Title workflow enforces this on every PR.

Each release:

  • Bumps VERSION in CMakeLists.txt (which flows into MOONBASE_CPP_VERSION and the User-Agent: moonbase-cpp/<version> header)
  • Updates CHANGELOG.md
  • Tags the commit and creates a GitHub Release (with the auto-generated source archives at https://github.com/<owner>/<repo>/archive/refs/tags/v<version>.tar.gz)

License

Released under the MIT License.