C++

The reference for the keygrant library 0.8.0, the KeyGrant SDK for C++, available as the CMake target keygrant::keygrant. The rules every SDK shares are in How every SDK behaves.

  • C++17 and CMake 3.20 or later.
  • Runs on Windows, macOS and Linux.
  • Depends on OpenSSL 3 and libcurl, plus nlohmann/json at build time.
  • Builds as a static library unless BUILD_SHARED_LIBS is on.

Release notes

Add it

Fetch it from its repository, keygrantdev/keygrant-cpp, in your CMake project:

CMakeLists.txt
include(FetchContent)FetchContent_Declare(keygrant  GIT_REPOSITORY https://github.com/keygrantdev/keygrant-cpp.git  GIT_TAG        0.8.0)FetchContent_MakeAvailable(keygrant)
target_link_libraries(my_app PRIVATE keygrant::keygrant)

Or, once installed (cmake --install), find it as a package:

CMakeLists.txt
find_package(keygrant 0.8 REQUIRED)target_link_libraries(my_app PRIVATE keygrant::keygrant)
  • A static library: when keygrant is one, find_package must also be able to find OpenSSL and libcurl.
  • libcurl older than 7.84: call curl_global_init(CURL_GLOBAL_DEFAULT) in main() before the app starts any thread.

Use it

C++
#include <keygrant/keygrant.hpp>
keygrant::LicenseConfig config;config.product = "my-product";config.apiBaseUrl = "https://api.keygrant.dev";config.publicJwk = R"({"keys":[...]})"; // the keys from /pubkeyconfig.major = 2;
keygrant::License license(config); // throws std::invalid_argument on a bad config
keygrant::LicenseStatus status = license.status();if (status.state != keygrant::LicenseState::Licensed && status.state != keygrant::LicenseState::Trial && status.state != keygrant::LicenseState::Unknown) { keygrant::ActivateResult result = license.activate(enteredKey); if (!result.ok) showError(keygrant::to_string(*result.error)); // "invalid-key", "limit", ...}
  • Every call blocks, so call it off the UI thread.
  • License is thread-safe, and cannot be copied or moved; create one per process. Calls on it run one at a time, except has, require, upgradeUrl and the two pickUp calls, which only read and run beside any call.

LicenseConfig

product

Required. The product's API ID: 1 to 64 bytes of UTF-8, not . or .., with no space, control character, /, \, ?, # or %.

apiBaseUrl

Required. Set it to https://api.keygrant.dev.

publicJwk

Required. The product's key set as a string: the {"keys": [...]} that /pubkey answers and the dashboard's Copy key set copies.

  • A JSON list of JWKs, one JWK, or its bare x also work.
  • A lease is verified by the key it names, or, naming none, by each key in turn.
major

This build's major version. 0, or anything below it, is read as 1, as KeyGrant reads a 0.x version, so a 0.x build can set 0 or 1.

  • Above 99,999, License's constructor throws std::invalid_argument.

Default 1

deviceName

What the license's device list calls this machine.

deviceHold

false turns device hold off, so no license is held to its device. See Device management.

Default true

httpTimeout

A std::chrono::milliseconds: how long one request may take. It must be greater than zero, and is capped at one day.

Default 15000

revokedLicenseFiles

A std::vector<std::string> of the ids of the license files this build refuses, matched exactly: how an app update stops an offline license file that leaked. See License files.

  • A stored file listed here reads as Invalid with reason RevokedFile, and importing one is refused.

Default empty ; Since 0.5.0

adapters

storage, registry, clock, fingerprint and http, each a std::shared_ptr. The default is used for any you leave empty.

Calls

activate(key)

Returns an ActivateResult with ok, and with error when ok is false: one of the activation errors, as a keygrant::ActivateError.

activate(key, options)

The same, with a keygrant::ActivateOptions, for a key only its buyer can activate (below).

Since 0.6.0

has(code)

Returns whether the license grants the entitlement code, offline from the stored lease. See Check an entitlement.

  • keygrant::grants(status.entitlements, code) answers the same of a status already in hand.
require(code)

Throws keygrant::MissingEntitlement, whose entitlement() is the code, when the license does not grant it.

importLicenseFile(content)

Returns an ImportLicenseFileResult (below).

Since 0.5.0

removeLicenseFile()

Drops the stored offline license file.

Since 0.5.0

use(meter, n = 1)

Returns a UseResult and never throws: ok with left, or error (Metered uses).

Since 0.7.0

pickUp(email)

Emails a six-digit code to the address, for key pickup. Returns a PickUpSentResult.

Since 0.8.0

pickUp(email, code)

Returns a PickUpKeysResult, whose keys are the address's keys.

Since 0.8.0

restoreByEmail(email, code)

Confirms the code and activates the key that fits this build. Returns a RestoreResult.

Since 0.8.0

status(forceOnline = false)

Returns a LicenseStatus (below). refresh() and startTrial() return one too.

deactivate()

Returns a DeactivateResult with released.

upgradeUrl(link)

Returns a std::optional<std::string>: std::nullopt when no key and activation are stored. See Sell an upgrade.

  • Otherwise it throws std::invalid_argument for a link that is not an absolute URL.

LicenseStatus

state, reason

The states and reasons, with the reason LicenseReason::RevokedFile (revoked-file) among them. keygrant::to_string converts each one to its string form.

key

The license key; on a status from a license file, the key the file records.

trialDaysLeft, trialEndsAt

On a trial: the whole days left, and when it ends, in Unix milliseconds.

test

true for a test license.

entitlements

A std::map, empty when the lease grants nothing. Each Entitlement reads with flag(), number() or text().

meters

A std::map too, empty on every status but a Licensed one from a key's lease that names meters (below).

stalled

true when an earlier call has not finished, so the status was read from storage.

source, licensedTo

On a status from a license file (below); empty on every status not from a license file.

Exceptions

std::invalid_argument

License's constructor, on a bad configuration (a major above 99,999 among them). Also upgradeUrl, for a link that is not an absolute URL.

keygrant::IoError

Activation and deactivation, when the license file cannot be read or saved.

keygrant::StalledError

deactivate(), importLicenseFile and removeLicenseFile(), while an earlier call is stuck.

the storage's exception

importLicenseFile and removeLicenseFile(), when the state cannot be read or saved.

keygrant::MissingEntitlement

require(code), when the license does not grant code.

Keys only their buyer can activate

When a key is locked to its buyer (Who can activate), pass what the customer typed to activate(key, options), as a keygrant::ActivateOptions whose email and code are each an optional string. The steps and rules are in How every SDK behaves. Since 0.6.0.

C++
keygrant::ActivateOptions options;options.email = emailField.text();options.code = codeField.text();const keygrant::ActivateResult result = license.activate(key, options);if (!result.ok) {  switch (*result.error) {    case keygrant::ActivateError::UserEmailRequired:    case keygrant::ActivateError::UserEmailMismatch: askForTheAddress(); break;  // then activate again with it    case keygrant::ActivateError::UserCodeRequired:    case keygrant::ActivateError::UserCodeInvalid: askForTheCode(); break;       // a code was just mailed to it    case keygrant::ActivateError::TooManyCodes:    case keygrant::ActivateError::TooManyAttempts: askToTryLater(); break;    case keygrant::ActivateError::TryAgain: askToRetry(); break;                  // nothing was activated    default: showTheOtherErrors(); break;  }}
  • activate(key) sends neither, and answers a locked key ActivateError::UserEmailRequired.
  • importLicenseFile(content, options) passes them on to a key file's key.
ActivateError::UserEmailRequired

Its word is user-email-required. The key is locked to its buyer and this device is new: ask for the email the key was sold to and call activate(key, options) with it.

ActivateError::UserEmailMismatch

Its word is user-email-mismatch. That is not the email the key was sold to (or it is longer than 254 UTF-16 units, and no request is made). KeyGrant never says what the right one is.

ActivateError::UserCodeRequired

Its word is user-code-required. The email was right and KeyGrant has just emailed a code to it: ask for the code and call activate(key, options) with the email and the code.

ActivateError::UserCodeInvalid

Its word is user-code-invalid. The code is wrong, expired (it lasts 10 minutes) or already used, or is not six digits (no request is made). Ask again, or call activate(key, options) with the email alone for a new code.

ActivateError::TooManyCodes

Its word is too-many-codes. Three codes have been emailed for this key this hour. Use the last one, or wait.

ActivateError::TooManyAttempts

Its word is too-many-attempts. Too many wrong emails or codes for this key this hour. Try again later.

ActivateError::TryAgain

Its word is try-again. The code could not be emailed just now, and nothing was activated. Try again; it is nobody's mistake.

Restoring a license without the key

For an App Store build, which may not take a typed key: pickUp(email) emails the customer a six-digit code, and restoreByEmail(email, code) activates the key that fits this build. How a pickup works is in How every SDK behaves. Since 0.8.0.

C++
// 1. Email a code to the address the key was sold to (the answer is the same either way).license.pickUp(email);
// 2. The customer types the code: restore.const keygrant::RestoreResult restored = license.restoreByEmail(email, code);if (!restored.ok) { if (restored.pickupError == keygrant::PickupError::WrongCode) askAgain(); if (restored.pickupError == keygrant::PickupError::CodeExpired) license.pickUp(email); // a new code if (restored.noKey) showNoKeyUnderThatAddress();}
  • To let the customer choose, call pickUp(email, code), show each of PickUpKeysResult::keys by its modelName, and activate the one chosen with activate(picked.key, {email, picked.activationCode}).
  • A RestoreResult holds a refusal in pickupError, noKey or error.
  • The errors are the pickup errors, in this SDK's own names. The requests are on the endpoints page.

License files

A customer can be sent a .kglicense file: a key file, which activates its key, or an offline license file, which licenses the app with no network. How each behaves is in How every SDK behaves, and what each costs under Delivered as in License models. Since 0.5.0.

Warning

Keep every key that signed a file in publicJwk. A build without it reads such a file as Invalid with reason UnknownKey.

  • keygrant::kLicenseFileExtension is .kglicense, for an open dialog's filter or a drop handler's test.
  • keygrant::kLicenseFileMediaType is application/vnd.keygrant.license, and keygrant::kMaxLicenseFileBytes is 16,384, the most a file may be.
  • All three are in <keygrant/license_file.hpp>, which <keygrant/keygrant.hpp> includes.
  • importLicenseFile takes the file's content, never its path: a std::string_view or a std::vector<std::uint8_t>, and optionally the ActivateOptions of a user-locked key, passed on to a key file's activation.

List the ids of leaked files in revokedLicenseFiles (Blocking a leaked file):

C++
config.revokedLicenseFiles = {"lf_8Hq2"};   // ids of files that leaked: an update adds them here

In Qt, take the file from a drop on the window or from an open dialog, read at most a license file's size, and import it off the UI thread:

C++
#include <keygrant/keygrant.hpp>#include <QDragEnterEvent>#include <QDropEvent>#include <QFile>#include <QFileDialog>#include <QFileInfo>#include <QFutureWatcher>#include <QMimeData>#include <QtConcurrent>
namespace {/// The first dropped local file that is a license file (`keygrant::kLicenseFileExtension`), else an empty string.QString licenseFileIn(const QMimeData* mime) { for (const QUrl& url : mime->urls()) { if (url.isLocalFile() && url.toLocalFile().endsWith(QStringLiteral(".kglicense"), Qt::CaseInsensitive)) return url.toLocalFile(); } return {};}
/// Reads the file, never more than a license file can be (a larger one is refused unread), and imports it.keygrant::ImportLicenseFileResult importFile(keygrant::License& license, const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) throw std::runtime_error("the file cannot be opened"); const QByteArray read = file.read(static_cast<qint64>(keygrant::kMaxLicenseFileBytes) + 1); return license.importLicenseFile(std::vector<std::uint8_t>(read.begin(), read.end()));}} // namespace
// In the window's constructor: setAcceptDrops(true);void LicenseWindow::dragEnterEvent(QDragEnterEvent* event) { if (!licenseFileIn(event->mimeData()).isEmpty()) event->acceptProposedAction();}
void LicenseWindow::dropEvent(QDropEvent* event) { const QString path = licenseFileIn(event->mimeData()); if (path.isEmpty()) return; event->acceptProposedAction(); importFrom(path);}
void LicenseWindow::openLicenseFile() { // "Open license file..." on the license screen const QString path = QFileDialog::getOpenFileName(this, tr("Open license file"), {}, tr("License files (*.kglicense)")); if (!path.isEmpty()) importFrom(path);}
void LicenseWindow::importFrom(const QString& path) { auto* watcher = new QFutureWatcher<keygrant::ImportLicenseFileResult>(this); connect(watcher, &QFutureWatcherBase::finished, this, [this, watcher] { try { const keygrant::ImportLicenseFileResult result = watcher->result(); if (result.ok) showLicensedTo(*result.licensedTo); // "Licensed to Ada Lovelace" else if (result.error) showError(keygrant::to_string(*result.error)); // "revoked-file", "upgrade", ... else showError(keygrant::to_string(*result.activateError)); // a key file: what activate() says } catch (const std::exception&) { showError("try-again"); // unreadable, a call before it has not finished, or the state not saved } watcher->deleteLater(); }); // Read and imported off the UI thread (a file on a network share can be slow); license_ is a // std::shared_ptr<keygrant::License>, held by the work. watcher->setFuture(QtConcurrent::run([license = license_, path] { return importFile(*license, path); }));}

wxWidgets (wxFileDropTarget), Win32 (WM_DROPFILES) and Cocoa (NSDraggingDestination) hand over a path the same way: read the file there, and give importLicenseFile its bytes.

What importLicenseFile answers

It verifies the file offline (Importing a file). The ImportLicenseFileResult has one of these, and keygrant::to_string gives each one's word:

  • ok, with the file's kind (LicenseFileKind::Key or Offline) and licensedTo;
  • error, a LicenseFileError below, with nothing stored;
  • activateError, for a key file whose key did not activate: what activate() answers.
LicenseFileError::Malformed

Its word is malformed. Not a license file's text, or its token or claims are broken, or it is more than kMaxLicenseFileBytes.

LicenseFileError::NotALicenseFile

Its word is not-a-license-file. A signed token, but not a license file (a lease, say).

LicenseFileError::BadSignature

Its word is bad-signature. Not signed by the key it names.

LicenseFileError::UnknownKey

Its word is unknown-key. Signed by a key this build does not carry (publicJwk): a newer one, after the product rotated its key, which an update of the app carries. A key file's notes print its key, which can be typed meanwhile.

LicenseFileError::WrongProduct

Its word is wrong-product. Another product's file.

LicenseFileError::RevokedFile

Its word is revoked-file. The file's id is listed in revokedLicenseFiles.

LicenseFileError::Upgrade

Its word is upgrade. An offline file that does not cover this build: this build is newer than the file covers.

LicenseFileError::Outdated

Its word is outdated. An offline file that does not cover this build: this build is older than the file covers.

It throws keygrant::StalledError when a call before it has not finished, and the storage's exception when the state cannot be read or the file cannot be saved.

The status of a stored file

A stored offline file answers every status() with Licensed, source == LicenseSource::File, the key, licensedTo and the file's entitlements, and never asks KeyGrant, refresh() included.

  • Show "Licensed to Ada Lovelace" from licensedTo.
  • A status from a file has no seat to deactivate: offer removeLicenseFile() instead, which drops the stored file while a key the device holds keeps working. deactivate() leaves the file alone.
  • Besides RevokedFile, a status from a file can be Invalid with reason Upgrade, Outdated, Tampered or UnknownKey. What each means, and which wins when the device also holds a key, is in The status of a stored file.

Metered uses

A license model can count uses of what it sells (Metered uses). The app spends them with use(meter, n), before the work they count, and does the work only when the spend is ok. Since 0.7.0.

C++
const keygrant::UseResult spent = license.use("exports");   // use("exports", 40) spends forty at once, whole or noneif (spent.ok) {  runExport();   // spent.left: what is left now, net of this device's unsynced spends} else {  switch (*spent.error) {    // The server's word: fewer left than asked (spent.left says how many; nullopt when it gave no number).    case keygrant::UseError::UsedUp: offerMoreExports(spent.left); break;    // KeyGrant could not be reached, and this is past what this device may spend without it.    case keygrant::UseError::OfflineLimit: askToConnect(); break;    // NoMeter, NotLicensed, Revoked, Device, Upgrade, Outdated, Storage, InvalidAmount.    default: showMeterProblem(*spent.error); break;  }}
const keygrant::LicenseStatus status = license.status(); // offline, from the leasestatus.meters.at("exports"); // MeterStatus{left 950, total 1000, offline 100, unsynced 0, resets false}
  • Like every call it blocks, so make it off your UI thread.
  • offline is true in the result when the device spent from its offline allowance.
  • n is a std::int64_t. Outside 1 to keygrant::kMaxMeterUses (1,000,000,000) it is InvalidAmount, and a meter code that is not 1 to 32 UTF-16 units is NoMeter, both without asking.
  • It never throws for an outcome: every one is in the UseResult. The call takes no cancellation: it runs to its bounded answer.
  • A request waits at most 5 seconds for KeyGrant, less when httpTimeout is shorter.
  • deactivate() sends the unsynced spends with the release.
  • How a spend works online and offline, and why a meter never takes the license away, is in How every SDK behaves.

When use does not spend

UseError::UsedUp

Its word is used-up. The meter has fewer uses left than asked. left says how many are left, when KeyGrant gave a number, so the app can offer fewer. It is KeyGrant's own word, not a guess.

UseError::OfflineLimit

Its word is offline-limit. KeyGrant could not be reached, and the spend is past this device's offline allowance for the meter or past what it last heard is left. left says what the device may still spend. Ask the customer to connect.

UseError::NoMeter

Its word is no-meter. The key has no meter with that code, or the code is empty or longer than 32 UTF-16 units.

UseError::NotLicensed

Its word is not-licensed. No key's lease licenses this build on this device, so nothing was asked. A device trial and an offline license file have no meters.

UseError::Revoked, Upgrade, Outdated, Device

KeyGrant's verdict on the key, this build or this device, as status() says it.

UseError::Storage

Its word is storage. The stored license (the state file) could not be read just now, or an adapter threw where there is no word for it. Try again shortly.

UseError::InvalidAmount

Its word is invalid-amount. n is not a whole number from 1 to kMaxMeterUses (1,000,000,000). This is a bug in the app: nothing was asked.

LicenseStatus::meters

A map by meter code, on a Licensed status from a key's lease that names meters, and empty on every other status. Each MeterStatus has:

left

What is left, net of this device's unsynced spends.

total

The uses the key holds, for good or for this billing period.

offline

This device's offline allowance.

unsynced

Uses this device spent that KeyGrant has not heard of yet.

resets

true for a meter that comes back each billing period.

Buying from the trial

C++
// The Buy button, while the app holds no key.openInBrowser("https://buy.stripe.com/your-payment-link");
// The customer types the key from the delivery email:if (license.activate(enteredKey).ok) showThanks();

The checkout creates the key and delivers it, and the customer types it into the app. See Buying from the trial.

When status() returns a trial, the trial reports its launch to KeyGrant at most once a day, on a thread of its own, so the call never waits for it. The app need not call startTrial() on each launch for Analytics to count the trial as come back (Device trials).

Keeping a trial through a reinstall

The default registry stash stores nothing. On Windows, WindowsRegistryStash stores the trial's start date under HKCU\Software\KeyGrant\<product>:

C++
#ifdef _WIN32config.adapters.registry = std::make_shared<keygrant::WindowsRegistryStash>("my-product");#endif