Skip to content

feat!: add validate_token_online with grace period and cadence#6

Merged
TobbenTM merged 5 commits into
mainfrom
TobbenTM/license-validation-grace-period
May 9, 2026
Merged

feat!: add validate_token_online with grace period and cadence#6
TobbenTM merged 5 commits into
mainfrom
TobbenTM/license-validation-grace-period

Conversation

@TobbenTM
Copy link
Copy Markdown
Member

@TobbenTM TobbenTM commented May 9, 2026

Summary

  • Adds licensing::validate_token_online that runs local checks first, then re-validates against POST /api/client/licenses/{productId}/validate. Offline-activated tokens never contact the API.
  • Two new 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.
  • Renames licensing::validate_token to licensing::validate_token_local to make the local-only semantics explicit (BREAKING). Renames internal detail::activation_query to detail::client_query since the same query params (metadata, platform, appVersion) now flow into both endpoints.

Test plan

  • ctest passes (26/26)
  • Live test against https://demo.moonbase.sh passes — covers the new validate_token_online round-trip end-to-end (MOONBASE_CPP_LIVE_TESTS=1)

TobbenTM added 5 commits May 9, 2026 09:30
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.
@TobbenTM TobbenTM merged commit fd4780b into main May 9, 2026
4 checks passed
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.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 9, 2026

🎉 This PR is included in version 2.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant