Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/macos-gamepad.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,9 @@ gamepad while authorization remains current. Existing virtual gamepads close
when their authorization expires or is revoked. A yearly activation needs
online validation after the broker restarts; this deliberately fails closed
when trusted elapsed time cannot be reconstructed.
Successful licensed gamepad creations are saved as pending usage and reported
through Polar's `increment_usage` field on the next license validation.
Validation with no new gamepads leaves Polar usage unchanged.

If creation returns `backend_unavailable`, inspect the launchd job with
`sudo launchctl print system/dev.lizardbyte.app.libvirtualhid`. If it returns
Expand Down
9 changes: 9 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,15 @@ a yearly subscription must reconnect to Polar before device creation; a lifetime
license can use the one-device outage fallback. A confirmed missing, revoked,
disabled, or mismatched entitlement invalidates the license and removes all
licensed virtual HID devices.
Each successful licensed gamepad creation adds one pending Polar usage unit.
The broker saves the pending count locally and sends it as `increment_usage`
with its next successful license validation (normally within 24 hours, or on
manual refresh). Routine validations do not increment usage when no gamepad
was created. Keyboard and mouse creation and GitHub Actions evaluation do not
count. Reporting is best effort: a lost response after Polar accepts an
increment can cause a later retry to count it twice, while a local state-write
failure or license deactivation before a pending count is sent can lose counts.
Do not use Polar's usage limit as an exact device-creation quota.
Purchase and account-management buttons use the compiled URLs in
`src/platform/windows/shared/lvh_windows_broker_config.hpp`.
Enable `Lock buttons` to click-to-toggle behavior for held inputs.
Expand Down
3 changes: 3 additions & 0 deletions docs/windows-driver.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,6 +411,9 @@ portal URL changes, then rebuild the Windows package. No Polar access token or
webhook secret is compiled into the client:
activation, validation, and deactivation use Polar's
[public customer license-key API](https://polar.sh/docs/features/benefits/license-keys).
Successful licensed gamepad creations are counted in the protected broker state
and reported through `increment_usage` on the next license validation. The
create request still completes without an online Polar request.
Those requests pin Polar's date-based API contract to `2026-04` with the
`Polar-Version` header. Before Polar removes that version, update
`polar_request_headers` in
Expand Down
5 changes: 3 additions & 2 deletions src/platform/macos/broker/libvirtualhid_macos_broker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,8 @@ namespace lvh::detail::macos_broker {
}
Message response;
bool evaluation = false;
if (!licenses.authorize_create(response, evaluation)) {
std::string authorized_key;
if (!licenses.authorize_create(response, evaluation, authorized_key)) {
static_cast<void>(send_message(fd, response));
::close(fd);
return;
Expand All @@ -346,7 +347,7 @@ namespace lvh::detail::macos_broker {
::close(fd);
return;
}
licenses.add_device(evaluation);
licenses.add_device(evaluation, authorized_key);
response.type = MessageType::response;
response.status = 0;
static_cast<void>(session.send(response));
Expand Down
7 changes: 5 additions & 2 deletions src/platform/macos/broker/license_manager.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <mutex>
#include <optional>
#include <string>
#include <string_view>
#include <thread>

namespace lvh::detail::macos_broker {
Expand All @@ -27,9 +28,9 @@ namespace lvh::detail::macos_broker {
LicenseManager &operator=(const LicenseManager &) = delete;

Message handle(const Message &request);
bool authorize_create(Message &response, bool &evaluation);
bool authorize_create(Message &response, bool &evaluation, std::string &authorized_key);
bool device_is_authorized(bool evaluation);
void add_device(bool evaluation);
void add_device(bool evaluation, std::string_view authorized_key);
void remove_device(bool evaluation);

private:
Expand All @@ -41,13 +42,15 @@ namespace lvh::detail::macos_broker {
std::string benefit_id;
std::string customer_email;
std::uint32_t activation_limit = 0;
std::uint32_t pending_usage = 0;
};

Message activate(const Message &request);
Message validate();
Message deactivate();
Message status();
bool licensed_locked() const;
bool save_state(const State &state) const;
void fill_status_locked(Message &response) const;
void background_validation(std::stop_token stop);

Expand Down
70 changes: 48 additions & 22 deletions src/platform/macos/broker/license_manager.mm
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#include <cstring>
#import <Foundation/Foundation.h>
#include <libvirtualhid/license.hpp>
#include <limits>
#include <string_view>
#include <sys/stat.h>
#include <utility>
Expand Down Expand Up @@ -178,6 +179,7 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
state.benefit_id = from_ns(json_string(saved, @"benefit_id"));
state.customer_email = from_ns(json_string(saved, @"customer_email"));
state.activation_limit = [saved[@"activation_limit"] unsignedIntValue];
state.pending_usage = [saved[@"pending_usage"] unsignedIntValue];
if (!state.key.empty() && !state.activation_id.empty() && state.status == "granted" && state.organization_id == broker_license::polar_organization_id && broker_license::benefit(state.benefit_id)) {
state_ = std::move(state);
}
Expand Down Expand Up @@ -217,6 +219,19 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
(validated_at_ && std::chrono::steady_clock::now() - *validated_at_ < broker_license::subscription_max_age);
}

bool LicenseManager::save_state(const State &state) const {
@autoreleasepool {
return write_protected_json(state_path, @{@"key": to_ns(state.key),
@"activation_id": to_ns(state.activation_id),
@"status": to_ns(state.status),
@"organization_id": to_ns(state.organization_id),
@"benefit_id": to_ns(state.benefit_id),
@"customer_email": to_ns(state.customer_email),
@"activation_limit": @(state.activation_limit),
@"pending_usage": @(state.pending_usage)});
}
}

void LicenseManager::fill_status_locked(Message &response) const {
response.active_devices = active_devices_;
if (!state_) {
Expand Down Expand Up @@ -292,13 +307,13 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
set_text(response.message, "License organization, benefit, or activation is not allowed");
return response;
}
if (!write_protected_json(state_path, @{@"key": to_ns(state.key),
@"activation_id": to_ns(state.activation_id),
@"status": to_ns(state.status),
@"organization_id": to_ns(state.organization_id),
@"benefit_id": to_ns(state.benefit_id),
@"customer_email": to_ns(state.customer_email),
@"activation_limit": @(state.activation_limit)})) {
{
std::lock_guard lock {mutex_};
if (state_ && state_->key == state.key) {
state.pending_usage = state_->pending_usage;
}
}
if (!save_state(state)) {
response.status = static_cast<int>(ErrorCode::backend_failure);
set_text(response.message, "Unable to securely save machine license");
return response;
Expand Down Expand Up @@ -332,11 +347,15 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
state = *state_;
}
@autoreleasepool {
auto result = polar_request(@"/v1/customer-portal/license-keys/validate", @ {
NSMutableDictionary *request_body = [@ {
@"key": to_ns(state.key),
@"organization_id": to_ns(broker_license::polar_organization_id),
@"activation_id": to_ns(state.activation_id)
});
} mutableCopy];
if (state.pending_usage != 0) {
request_body[@"increment_usage"] = @(state.pending_usage);
}
auto result = polar_request(@"/v1/customer-portal/license-keys/validate", request_body);
if (!result.transport_ok || result.status != 200 || !result.body || !result.trusted_time) {
{
std::lock_guard lock {mutex_};
Expand Down Expand Up @@ -378,17 +397,12 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
state.status = new_status;
state.benefit_id = new_benefit;
state.activation_limit = [result.body[@"limit_activations"] unsignedIntValue];
state.pending_usage = 0;
NSDictionary *customer = result.body[@"customer"];
if ([customer isKindOfClass:[NSDictionary class]]) {
state.customer_email = from_ns(json_string(customer, @"email"));
}
if (!write_protected_json(state_path, @{@"key": to_ns(state.key),
@"activation_id": to_ns(state.activation_id),
@"status": to_ns(state.status),
@"organization_id": to_ns(state.organization_id),
@"benefit_id": to_ns(state.benefit_id),
@"customer_email": to_ns(state.customer_email),
@"activation_limit": @(state.activation_limit)})) {
if (!save_state(state)) {
auto response = status();
response.status = static_cast<int>(ErrorCode::backend_failure);
set_text(response.message, "Unable to securely save validated license");
Expand Down Expand Up @@ -466,13 +480,14 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
}
}

bool LicenseManager::authorize_create(Message &response, bool &evaluation) {
bool LicenseManager::authorize_create(Message &response, bool &evaluation, std::string &authorized_key) {
response.type = MessageType::response;
std::lock_guard lock {mutex_};
fill_status_locked(response);
if (licensed_locked()) {
if (online_confirmed_ || active_licensed_devices_ == 0) {
evaluation = false;
authorized_key = state_->key;
return true;
}
response.status = static_cast<int>(ErrorCode::network_unavailable);
Expand Down Expand Up @@ -516,11 +531,22 @@ bool valid_c_string(const std::array<char, max_text_size> &value) {
return !unavailable_since_ || !broker_license::outage_retention_elapsed(std::chrono::steady_clock::now() - *unavailable_since_);
}

void LicenseManager::add_device(bool evaluation) {
std::lock_guard lock {mutex_};
++active_devices_;
if (!evaluation) {
++active_licensed_devices_;
void LicenseManager::add_device(bool evaluation, std::string_view authorized_key) {
std::lock_guard operation_lock {operation_mutex_};
std::optional<State> state;
{
std::lock_guard lock {mutex_};
++active_devices_;
if (!evaluation) {
++active_licensed_devices_;
if (state_ && state_->key == authorized_key && state_->pending_usage < std::numeric_limits<std::uint32_t>::max()) {
++state_->pending_usage;
state = *state_;
}
}
}
if (state) {
static_cast<void>(save_state(*state));
}
}

Expand Down
59 changes: 52 additions & 7 deletions src/platform/windows/broker/libvirtualhid_broker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,7 @@ namespace lvh::detail::windows_broker_service {
std::string benefit_id;
std::string customer_email;
std::uint32_t activation_limit = 0;
std::uint32_t pending_usage = 0;
// Audit timestamp supplied by Polar, not the local machine clock.
std::uint64_t validated_at = 0;
// The boot-session marker and uptime at validation let the broker advance
Expand All @@ -641,6 +642,18 @@ namespace lvh::detail::windows_broker_service {
std::uint64_t validated_uptime_ms = 0;
};

nlohmann::json polar_validation_body(const PolarLicenseState &state) {
nlohmann::json body {
{"key", state.license_key},
{"organization_id", std::string {lvh::windows::broker_config::polar_organization_id}},
{"activation_id", state.activation_id},
};
if (state.pending_usage != 0U) {
body["increment_usage"] = state.pending_usage;
}
return body;
}

std::string serialize_license_state(const PolarLicenseState &state) {
std::ostringstream serialized;
serialized << "provider=" << state.provider << "\n";
Expand All @@ -652,6 +665,7 @@ namespace lvh::detail::windows_broker_service {
serialized << "benefit_id=" << state.benefit_id << "\n";
serialized << "customer_email=" << state.customer_email << "\n";
serialized << "activation_limit=" << state.activation_limit << "\n";
serialized << "pending_usage=" << state.pending_usage << "\n";
serialized << "validated_at=" << state.validated_at << "\n";
serialized << "boot_marker=" << state.boot_marker << "\n";
serialized << "validated_uptime_ms=" << state.validated_uptime_ms << "\n";
Expand Down Expand Up @@ -791,6 +805,11 @@ namespace lvh::detail::windows_broker_service {
state.customer_email = value;
} else if (key == "activation_limit") {
state.activation_limit = static_cast<std::uint32_t>(parse_uint64(value).value_or(0));
} else if (key == "pending_usage") {
state.pending_usage = static_cast<std::uint32_t>(std::min(
parse_uint64(value).value_or(0),
static_cast<std::uint64_t>(std::numeric_limits<std::uint32_t>::max())
));
} else if (key == "validated_at") {
state.validated_at = parse_uint64(value).value_or(0);
} else if (key == "boot_marker") {
Expand Down Expand Up @@ -1389,9 +1408,11 @@ namespace lvh::detail::windows_broker_service {
return response;
}

std::string authorized_license_id;
const auto [authorization_status, github_actions_evaluation] = authorize_device_create(
response.license,
response.message
response.message,
authorized_license_id
);
if (authorization_status != LvhWindowsBrokerStatusCode::success) {
response.status = std::to_underlying(authorization_status);
Expand Down Expand Up @@ -1471,6 +1492,9 @@ namespace lvh::detail::windows_broker_service {
}

response.status = std::to_underlying(LvhWindowsBrokerStatusCode::success);
if (!github_actions_evaluation && request.device.device_type == LVH_WINDOWS_DEVICE_GAMEPAD) {
record_gamepad_creation(authorized_license_id);
}
copy_c_string(
response.message,
github_actions_evaluation ?
Expand Down Expand Up @@ -1685,6 +1709,13 @@ namespace lvh::detail::windows_broker_service {
return response;
}

{
std::lock_guard lock {mutex_};
if (license_state_ && license_state_->license_key_id == new_state.license_key_id) {
new_state.pending_usage = license_state_->pending_usage;
}
}

std::string authorization_error;
if (const auto authorization_result = accept_trusted_license_time(new_state, api_result.server_time, authorization_error); authorization_result != TrustedLicenseTimeResult::success) {
response.status = std::to_underlying(
Expand Down Expand Up @@ -2031,6 +2062,21 @@ namespace lvh::detail::windows_broker_service {
));
}

void record_gamepad_creation(std::string_view authorized_license_id) {
std::lock_guard operation_lock {license_operation_mutex_};
PolarLicenseState state;
{
std::lock_guard lock {mutex_};
if (!license_state_ || license_state_->license_key_id != authorized_license_id || license_state_->pending_usage == std::numeric_limits<std::uint32_t>::max()) {
return;
}
++license_state_->pending_usage;
state = *license_state_;
}
std::string save_error;
static_cast<void>(save_license_state(state, save_error));
}

bool license_is_active_locked() const {
return license_state_ &&
license_allowed(*license_state_) &&
Expand Down Expand Up @@ -2066,11 +2112,7 @@ namespace lvh::detail::windows_broker_service {

auto api_result = post_polar_license_request(
L"/v1/customer-portal/license-keys/validate",
nlohmann::json {
{"key", state.license_key},
{"organization_id", std::string {lvh::windows::broker_config::polar_organization_id}},
{"activation_id", state.activation_id},
}
polar_validation_body(state)
);
if (!api_result.transport_ok) {
mark_license_validation_unavailable();
Expand Down Expand Up @@ -2167,7 +2209,8 @@ namespace lvh::detail::windows_broker_service {

std::pair<LvhWindowsBrokerStatusCode, bool> authorize_device_create(
LvhWindowsBrokerLicenseStatus &license,
std::array<char, LVH_WINDOWS_BROKER_MAX_MESSAGE_SIZE> &message
std::array<char, LVH_WINDOWS_BROKER_MAX_MESSAGE_SIZE> &message,
std::string &authorized_license_id
) {
{
std::lock_guard lock {mutex_};
Expand Down Expand Up @@ -2215,6 +2258,8 @@ namespace lvh::detail::windows_broker_service {
return {LvhWindowsBrokerStatusCode::license_invalid, false};
}

authorized_license_id = license_state_->license_key_id;

if (license_online_confirmed_) {
fill_license_status_locked(license);
copy_c_string(message, "License validated online.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ namespace lvh::detail::test {

BrokerPolarResult broker_polar_scenario(BrokerPolarScenario scenario);

struct BrokerUsageResult {
bool pending_usage_round_trips = false;
bool routine_validation_omits_increment = false;
bool pending_validation_increments_usage = false;
};

BrokerUsageResult broker_usage_policy();

struct BrokerSubscriptionValidationResult {
bool yearly_benefit_is_subscription_backed = false;
bool subscription_validation_before_deadline_is_current = false;
Expand Down
Loading
Loading