// AppsXi Level 1 license helper for C++ and JUCE apps (PRD 15.4, LIC-5). // // Needs OpenSSL 1.1.1+ (libcrypto) for Ed25519 and SHA-256. HTTP is pluggable: pass your own // function (for example one built on juce::URL), or compile with APPXI_USE_CURL to use libcurl. // // appxi::License lic({"pk_live_…", {""}, "my-app", dataDir}); // auto r = lic.activate("AXI-…"); // once, online // if (auto e = lic.current()) unlock(*e); // every launch, offline // if (lic.needsRefresh()) lic.refresh(); #pragma once #include #include #include #include #include namespace appxi { /** POSTs a JSON body with an Authorization header; returns the response body ("" on failure). */ using HttpPost = std::function; struct Options { std::string productKey; // pk_live_… or pk_test_… std::vector publicKeys; // base64url Ed25519 keys ("x" in AppsXi's JWK set) std::string appSlug; std::string storageDir; // where appxi-license.json is kept std::string apiBase = "https://appsxi.com"; HttpPost http; // empty: libcurl (APPXI_USE_CURL) }; struct Entitlement { std::string status; // "active" or "trial" std::string license; std::string buyer; std::string environment; // "live" or "sandbox" std::time_t expiresAt = 0; std::time_t trialExpiresAt = 0; // 0 unless a trial int seats = 1; }; struct Validation { bool valid = false; std::string reason; // seat_limit, revoked, expired, unknown_license, trial_used_on_device, network, … int seatsUsed = 0; int seatsMax = 0; std::optional entitlement; }; struct TrialStatus { bool inTrial = false; int daysLeft = 0; bool expired = false; }; class License { public: explicit License(Options options); /** SHA-256 of the machine id with this app's product key and slug. Never the raw id. */ std::string deviceId() const; /** Validates a key online, takes a seat on this machine and caches the signed token. */ Validation activate(const std::string& licenseKey); /** Validates the cached key again. */ Validation refresh(); /** The verified entitlement from the cached token, offline. Empty if missing, tampered or expired. */ std::optional current(std::time_t now = std::time(nullptr)) const; /** True when the token is missing or has less than seven days left. */ bool needsRefresh(std::time_t now = std::time(nullptr)) const; TrialStatus trialStatus(std::time_t now = std::time(nullptr)) const; /** Open with juce::URL(...).launchInDefaultBrowser(). */ std::string checkoutUrl() const; /** License-bound appcast URL for paid apps (WinSparkle or Sparkle); empty before activation. */ std::string updateFeedUrl(const std::string& platform, const std::string& channel = "stable") const; /** Removes the cached key and token. */ void forget() const; /** Verifies a compact EdDSA JWT and returns its payload JSON, or "" if invalid. */ static std::string verifyToken(const std::string& token, const std::vector& publicKeys); private: Options o; std::string file; struct Cache { std::string key, token, trialExpiresAt, feedBase, feedToken; }; std::optional load() const; void save(const Cache& c) const; }; } // namespace appxi