feat!: add validate_token_online with grace period and cadence#6
Merged
Conversation
Adds online re-validation against POST /api/client/licenses/{productId}/validate,
gated by two new licensing_options: online_validation_min_interval (default
5 minutes, throttles API calls) and online_validation_grace_period (default
7 days, hard offline-tolerance threshold). Local validation always runs first;
offline-activated tokens never contact the API; transient transport failures
fall back to the local result while within grace; definitive license errors
always propagate.
BREAKING CHANGE: licensing::validate_token has been renamed to
licensing::validate_token_local to make the local-only semantics explicit
alongside the new validate_token_online.
…dence MoonbaseUnlockStatus::tryLoadStoredLicense now defaults to validate_token_online, persists the refreshed token so the cadence/grace clock advances across restarts, and catches transport-past-grace failures as "not unlocked" instead of letting them propagate into the host. Adds an online=false escape hatch for callers that need pure local validation. Updates docs/juce.md to describe the new defaults, the two licensing_options knobs (online_validation_min_interval, online_validation_grace_period), the offline-token guarantee, and the synchronous-call caveat.
… host The synchronous tryLoadStoredLicense path is fine for CLI tools and standalone apps but a real plugin can't afford to block the host's plugin-load thread on libcurl. The new async variant runs local validation inline (so the plugin loads optimistically unlocked from cached state) and performs the online check on a juce::Thread, marshalling the result back to the message thread via callAsync. A juce::WeakReference protects the continuation from a destroyed bridge, and licensing_ is now held via shared_ptr so the background thread can safely outlive a teardown mid-request. The result enum (Refreshed / LockedInvalid / LockedExpired / Unreachable / OfflineToken / NoStoredLicense / LocalInvalid) lets UI code distinguish "server unreachable past grace" from "license revoked" if it cares; for most callers, just calling refreshLabel() (or equivalent) on the bridge's unlock state is enough. PluginActivationComponent now uses the async variant. docs/juce.md documents both code paths and recommends async for plugins.
- validate_token_online's throttle skip now requires the token age to be within both online_validation_min_interval AND online_validation_grace_period. Previously a min_interval longer than the grace period silently extended "max age without an online check" past its advertised limit (e.g. min=30d, grace=7d would never revalidate during days 1-29). Adds a test that pins the new behavior with min_interval > grace_period. - tryLoadStoredLicenseAsync now always marshals state mutation and the callback through juce::MessageManager::callAsync, including the early-return paths (NoStoredLicense, LocalInvalid, OfflineToken). The doc promised message-thread delivery but those cases fired synchronously on the caller's thread, which is a problem because hosts often construct AudioProcessors off the message thread. - Adds an atomic generation counter on the bridge. tryLoadStoredLicense*, pollPendingActivation (on success), and clearLicense bump it; async continuations capture the value at request time and silently drop both state mutation and callback if a newer call has superseded them. This prevents a slow online check from resurrecting a license the user just cleared, or clobbering a freshly activated one.
…xample The standalone example app now wires a file_license_store under the platform's per-user app data directory (so activation actually survives restart) and displays the license's validated_at claim alongside the email and expiry — handy for seeing the cadence/grace clock advance on each launch.
github-actions Bot
pushed a commit
that referenced
this pull request
May 9, 2026
# [2.0.0](v1.1.0...v2.0.0) (2026-05-09) * feat!: add validate_token_online with grace period and cadence ([#6](#6)) ([fd4780b](fd4780b)) ### BREAKING CHANGES * licensing::validate_token has been renamed to licensing::validate_token_local to make the local-only semantics explicit alongside the new validate_token_online. * docs(juce): use validate_token_online in bridge and document grace/cadence MoonbaseUnlockStatus::tryLoadStoredLicense now defaults to validate_token_online, persists the refreshed token so the cadence/grace clock advances across restarts, and catches transport-past-grace failures as "not unlocked" instead of letting them propagate into the host. Adds an online=false escape hatch for callers that need pure local validation. Updates docs/juce.md to describe the new defaults, the two licensing_options knobs (online_validation_min_interval, online_validation_grace_period), the offline-token guarantee, and the synchronous-call caveat. * feat(juce): add async tryLoadStoredLicenseAsync that never blocks the host The synchronous tryLoadStoredLicense path is fine for CLI tools and standalone apps but a real plugin can't afford to block the host's plugin-load thread on libcurl. The new async variant runs local validation inline (so the plugin loads optimistically unlocked from cached state) and performs the online check on a juce::Thread, marshalling the result back to the message thread via callAsync. A juce::WeakReference protects the continuation from a destroyed bridge, and licensing_ is now held via shared_ptr so the background thread can safely outlive a teardown mid-request. The result enum (Refreshed / LockedInvalid / LockedExpired / Unreachable / OfflineToken / NoStoredLicense / LocalInvalid) lets UI code distinguish "server unreachable past grace" from "license revoked" if it cares; for most callers, just calling refreshLabel() (or equivalent) on the bridge's unlock state is enough. PluginActivationComponent now uses the async variant. docs/juce.md documents both code paths and recommends async for plugins. * fix: address review on grace/throttle interaction and async correctness - validate_token_online's throttle skip now requires the token age to be within both online_validation_min_interval AND online_validation_grace_period. Previously a min_interval longer than the grace period silently extended "max age without an online check" past its advertised limit (e.g. min=30d, grace=7d would never revalidate during days 1-29). Adds a test that pins the new behavior with min_interval > grace_period. - tryLoadStoredLicenseAsync now always marshals state mutation and the callback through juce::MessageManager::callAsync, including the early-return paths (NoStoredLicense, LocalInvalid, OfflineToken). The doc promised message-thread delivery but those cases fired synchronously on the caller's thread, which is a problem because hosts often construct AudioProcessors off the message thread. - Adds an atomic generation counter on the bridge. tryLoadStoredLicense*, pollPendingActivation (on success), and clearLicense bump it; async continuations capture the value at request time and silently drop both state mutation and callback if a newer call has superseded them. This prevents a slow online check from resurrecting a license the user just cleared, or clobbering a freshly activated one. * docs(juce): persist license to disk and surface validated_at in the example The standalone example app now wires a file_license_store under the platform's per-user app data directory (so activation actually survives restart) and displays the license's validated_at claim alongside the email and expiry — handy for seeing the cadence/grace clock advance on each launch.
|
🎉 This PR is included in version 2.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
licensing::validate_token_onlinethat runs local checks first, then re-validates againstPOST /api/client/licenses/{productId}/validate. Offline-activated tokens never contact the API.licensing_options:online_validation_min_interval(default 5 min) throttles API calls;online_validation_grace_period(default 7 days) is the hard offline-tolerance threshold — transient transport failures fall back to the local result while within grace, and definitive license errors always propagate.licensing::validate_tokentolicensing::validate_token_localto make the local-only semantics explicit (BREAKING). Renames internaldetail::activation_querytodetail::client_querysince the same query params (metadata, platform, appVersion) now flow into both endpoints.Test plan
ctestpasses (26/26)https://demo.moonbase.shpasses — covers the newvalidate_token_onlineround-trip end-to-end (MOONBASE_CPP_LIVE_TESTS=1)