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_LIBSis on.
Add it
Fetch it from its repository, keygrantdev/keygrant-cpp, in your CMake project:
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:
find_package(keygrant 0.8 REQUIRED)target_link_libraries(my_app PRIVATE keygrant::keygrant) - A static library: when
keygrantis one,find_packagemust also be able to find OpenSSL and libcurl. - libcurl older than 7.84: call
curl_global_init(inCURL_GLOBAL_DEFAULT) main()before the app starts any thread.
Use it
#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.
Licenseis thread-safe, and cannot be copied or moved; create one per process. Calls on it run one at a time, excepthas,require,upgradeUrland the twopickUpcalls, which only read and run beside any call.
LicenseConfig
Required. The product's API ID: 1 to 64 bytes of UTF-8, not . or .., with no space, control character, /, \, ?, # or %.
Required. Set it to https://api.keygrant.dev.
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
xalso work. - A lease is verified by the key it names, or, naming none, by each key in turn.
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 throwsstd::invalid_argument.
What the license's device list calls this machine.
false turns device hold off, so no license is held to its device. See Device management.
A std::chrono::milliseconds: how long one request may take. It must be greater than zero, and is capped at one day.
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
Invalidwith reasonRevokedFile, and importing one is refused.
storage, registry, clock, fingerprint and http, each a std::shared_ptr. The default is used for any you leave empty.
Calls
Returns an ActivateResult with ok, and with error when ok is false: one of the activation errors, as a keygrant::ActivateError.
The same, with a keygrant::ActivateOptions, for a key only its buyer can activate (below).
Returns whether the license grants the entitlement code, offline from the stored lease. See Check an entitlement.
keygrant::answers the same of a status already in hand.grants( status.entitlements, code)
Throws keygrant::MissingEntitlement, whose entitlement() is the code, when the license does not grant it.
Returns an ImportLicenseFileResult (below).
Drops the stored offline license file.
Returns a UseResult and never throws: ok with left, or error (Metered uses).
Emails a six-digit code to the address, for key pickup. Returns a PickUpSentResult.
Returns a PickUpKeysResult, whose keys are the address's keys.
Confirms the code and activates the key that fits this build. Returns a RestoreResult.
Returns a LicenseStatus (below). refresh() and startTrial() return one too.
Returns a DeactivateResult with released.
Returns a std::optional<std::string>: std::nullopt when no key and activation are stored. See Sell an upgrade.
- Otherwise it throws
std::invalid_argumentfor a link that is not an absolute URL.
LicenseStatus
The states and reasons, with the reason LicenseReason::RevokedFile (revoked-file) among them. keygrant::to_string converts each one to its string form.
The license key; on a status from a license file, the key the file records.
On a trial: the whole days left, and when it ends, in Unix milliseconds.
true for a test license.
A std::map, empty when the lease grants nothing. Each Entitlement reads with flag(), number() or text().
A std::map too, empty on every status but a Licensed one from a key's lease that names meters (below).
true when an earlier call has not finished, so the status was read from storage.
On a status from a license file (below); empty on every status not from a license file.
Exceptions
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.
Activation and deactivation, when the license file cannot be read or saved.
deactivate(), importLicenseFile and removeLicenseFile(), while an earlier call is stuck.
importLicenseFile and removeLicenseFile(), when the state cannot be read or saved.
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.
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 keyActivateError::UserEmailRequired.importLicenseFile(content, options)passes them on to a key file's key.
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.
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.
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.
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.
Its word is too-many-codes. Three codes have been emailed for this key this hour. Use the last one, or wait.
Its word is too-many-attempts. Too many wrong emails or codes for this key this hour. Try again later.
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.
// 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 ofPickUpKeysResult::keysby itsmodelName, and activate the one chosen withactivate(picked.key, {email, picked.activationCode}). - A
RestoreResultholds a refusal inpickupError,noKeyorerror. - 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::kLicenseFileExtensionis.kglicense, for an open dialog's filter or a drop handler's test.keygrant::kLicenseFileMediaTypeisapplication/vnd.keygrant.license, andkeygrant::kMaxLicenseFileBytesis 16,384, the most a file may be.- All three are in
<keygrant/license_file.hpp>, which<keygrant/keygrant.hpp>includes. importLicenseFiletakes the file's content, never its path: astd::string_viewor astd::vector<std::uint8_t>, and optionally theActivateOptionsof a user-locked key, passed on to a key file's activation.
List the ids of leaked files in revokedLicenseFiles (Blocking a leaked file):
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:
#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'skind(LicenseFileKind::KeyorOffline) andlicensedTo;error, aLicenseFileErrorbelow, with nothing stored;activateError, for a key file whose key did not activate: whatactivate()answers.
Its word is malformed. Not a license file's text, or its token or claims are broken, or it is more than kMaxLicenseFileBytes.
Its word is not-a-license-file. A signed token, but not a license file (a lease, say).
Its word is bad-signature. Not signed by the key it names.
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.
Its word is wrong-product. Another product's file.
Its word is revoked-file. The file's id is listed in revokedLicenseFiles.
Its word is upgrade. An offline file that does not cover this build: this build is newer than the file covers.
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 beInvalidwith reasonUpgrade,Outdated,TamperedorUnknownKey. 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.
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.
offlineistruein the result when the device spent from its offline allowance.nis astd::int64_t. Outside 1 tokeygrant::kMaxMeterUses(1,000,000,000) it isInvalidAmount, and a meter code that is not 1 to 32 UTF-16 units isNoMeter, 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
httpTimeoutis 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
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.
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.
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.
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.
KeyGrant's verdict on the key, this build or this device, as status() says it.
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.
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:
What is left, net of this device's unsynced spends.
The uses the key holds, for good or for this billing period.
This device's offline allowance.
Uses this device spent that KeyGrant has not heard of yet.
true for a meter that comes back each billing period.
Buying from the trial
// 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\:
#ifdef _WIN32config.adapters.registry = std::make_shared<keygrant::WindowsRegistryStash>("my-product");#endif