// Drop-in C++ client library for the Production Board HTTP API. // // Save this file alongside your source as `ProdClient.hpp` and use // the ProdClient class: // // #include "ProdClient.hpp" // prod_client::ProdClient c("pat_..."); // auto rows = c.AccountList({{}}); // auto fresh = c.AccountCreate({{ {{"name", "Example GmbH"}} }}); // // Every endpoint exposed by the HTTP API is wrapped as a typed // `` method on ProdClient. List endpoints take a // ListOpts; get/update/delete endpoints take the row id as their // first argument. // // Header-only. Requires libcurl headers + linker flag `-lcurl`. The // JSON encoder / decoder is bundled inline so no other dependency is // needed. Targets C++17 + libcurl 7.x. // // Provided as-is, with no warranty. Vendor freely; modify as needed. // // DO NOT EDIT THIS FILE MANUALLY - re-download from the docs site. // Local edits will be overwritten by the once-per-day version check. #ifndef prod_client_CLIENT_HPP_ #define prod_client_CLIENT_HPP_ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace prod_client { // ── Identity (substituted at generation time) ──────────────────────── inline constexpr const char* kAppSlug = "prod"; inline constexpr const char* kAppName = "Production Board"; inline constexpr const char* kModuleName = "prod_client"; inline constexpr const char* kClientVersion = "0.3.12"; inline constexpr const char* kLanguage = "cpp"; inline constexpr const char* kDefaultBase = "https://deploysition.cloud"; // Per-type metadata baked at generation time. Available at runtime // when calling code needs to know the legal filters / sort columns / // max_limit for a model without a second round-trip. Decode with // ProdClient::ParseJson. inline const char* TypesJson() { static const char* kBlob = R"JSON_BLOB({"board":{"ops":["list","read","create","update","delete"],"create_fields":["name","description","accent","settings","tags","columns"],"update_fields":["name","description","accent","settings","tags","columns"],"allowed_filters":["data__name","data__accent","data__tags","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__name"],"default_sort":"created_at","max_limit":50,"fields":[{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"accent","type":"enum","values":["slate","gray","blue","indigo","violet","fuchsia","amber","orange","emerald","green","rose","red"]},{"name":"settings","type":"dict"},{"name":"description","type":"string","max_len":2000}]},"card":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"update_fields":["title","description","status","position","priority","tags","assignee","due_date","board_id"],"allowed_filters":["data__status","data__priority","data__tags","data__assignee","data__board_id","status","is_archived","owned_by"],"allowed_sorts":["created_at","updated_at","data__position","data__status","data__priority","data__due_date"],"default_sort":"data__position","max_limit":200,"fields":[{"name":"tags","type":"tags"},{"name":"title","type":"string","max_len":200},{"name":"status","type":"string","max_len":64},{"name":"assignee","type":"string","max_len":64},{"name":"board_id","type":"string","max_len":64,"ref":{"type":"board","owned":true,"optional":true}},{"name":"due_date","type":"string","max_len":32},{"name":"position","type":"number"},{"name":"priority","type":"enum","values":["low","medium","high","critical"]},{"name":"description","type":"string","max_len":4000}]}})JSON_BLOB"; return kBlob; } // ── Json: tiny self-contained encoder + decoder ────────────────────── // Recursive variant. Object keeps insertion order so encoded payloads // round-trip cleanly with the rest of the toolchain. class Json { public: enum class Kind { Null, Bool, Number, String, Array, Object }; Json() = default; Json(std::nullptr_t) : kind_(Kind::Null) {} Json(bool v) : kind_(Kind::Bool), bool_(v) {} Json(int v) : kind_(Kind::Number), num_(static_cast(v)) {} Json(long v) : kind_(Kind::Number), num_(static_cast(v)) {} Json(long long v) : kind_(Kind::Number), num_(static_cast(v)) {} Json(double v) : kind_(Kind::Number), num_(v) {} Json(const char* v) : kind_(Kind::String), str_(v ? v : "") {} Json(const std::string& v) : kind_(Kind::String), str_(v) {} Json(std::string&& v) : kind_(Kind::String), str_(std::move(v)) {} static Json Array() { Json j; j.kind_ = Kind::Array; return j; } static Json Object() { Json j; j.kind_ = Kind::Object; return j; } static Json FromInitializerObject(std::initializer_list> kvs) { Json j = Object(); for (auto& kv : kvs) j.Set(kv.first, kv.second); return j; } Kind GetKind() const { return kind_; } bool IsNull() const { return kind_ == Kind::Null; } bool IsBool() const { return kind_ == Kind::Bool; } bool IsNumber() const { return kind_ == Kind::Number; } bool IsString() const { return kind_ == Kind::String; } bool IsArray() const { return kind_ == Kind::Array; } bool IsObject() const { return kind_ == Kind::Object; } bool AsBool(bool def = false) const { return IsBool() ? bool_ : def; } double AsNumber(double def = 0.0) const { return IsNumber() ? num_ : def; } long long AsInt(long long def = 0) const { return IsNumber() ? static_cast(num_) : def; } std::string AsString(const std::string& def = "") const { return IsString() ? str_ : def; } const std::vector& AsArray() const { return arr_; } std::vector& AsArray() { return arr_; } void Push(const Json& v) { if (kind_ != Kind::Array) { kind_ = Kind::Array; arr_.clear(); } arr_.push_back(v); } void Set(const std::string& key, const Json& v) { if (kind_ != Kind::Object) { kind_ = Kind::Object; obj_keys_.clear(); obj_vals_.clear(); } for (size_t i = 0; i < obj_keys_.size(); ++i) { if (obj_keys_[i] == key) { obj_vals_[i] = v; return; } } obj_keys_.push_back(key); obj_vals_.push_back(v); } bool Has(const std::string& key) const { if (!IsObject()) return false; for (size_t i = 0; i < obj_keys_.size(); ++i) if (obj_keys_[i] == key) return true; return false; } const Json& Get(const std::string& key) const { static const Json kNull; if (!IsObject()) return kNull; for (size_t i = 0; i < obj_keys_.size(); ++i) if (obj_keys_[i] == key) return obj_vals_[i]; return kNull; } const std::vector& Keys() const { return obj_keys_; } const std::vector& Values() const { return obj_vals_; } std::string Encode() const { std::ostringstream os; EncodeTo(os); return os.str(); } static Json Parse(const std::string& src) { size_t i = 0; SkipWs(src, i); Json out = ParseValue(src, i); SkipWs(src, i); return out; } private: Kind kind_ = Kind::Null; bool bool_ = false; double num_ = 0.0; std::string str_; std::vector arr_; std::vector obj_keys_; std::vector obj_vals_; void EncodeTo(std::ostringstream& os) const { switch (kind_) { case Kind::Null: os << "null"; break; case Kind::Bool: os << (bool_ ? "true" : "false"); break; case Kind::Number: { if (num_ == static_cast(num_) && std::abs(num_) < 1e15) { os << static_cast(num_); } else { os.precision(15); os << num_; } break; } case Kind::String: EncodeString(os, str_); break; case Kind::Array: { os << "["; for (size_t i = 0; i < arr_.size(); ++i) { if (i) os << ","; arr_[i].EncodeTo(os); } os << "]"; break; } case Kind::Object: { os << "{"; for (size_t i = 0; i < obj_keys_.size(); ++i) { if (i) os << ","; EncodeString(os, obj_keys_[i]); os << ":"; obj_vals_[i].EncodeTo(os); } os << "}"; break; } } } static void EncodeString(std::ostringstream& os, const std::string& s) { os << "\""; for (size_t i = 0; i < s.size(); ++i) { unsigned char c = static_cast(s[i]); switch (c) { case '"': os << "\\\""; break; case '\\': os << "\\\\"; break; case '\b': os << "\\b"; break; case '\f': os << "\\f"; break; case '\n': os << "\\n"; break; case '\r': os << "\\r"; break; case '\t': os << "\\t"; break; default: if (c < 0x20) { char buf[8]; std::snprintf(buf, sizeof(buf), "\\u%04x", c); os << buf; } else { os << static_cast(c); } } } os << "\""; } static void SkipWs(const std::string& s, size_t& i) { while (i < s.size() && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r')) ++i; } static Json ParseValue(const std::string& s, size_t& i) { SkipWs(s, i); if (i >= s.size()) throw std::runtime_error("json: unexpected end"); char c = s[i]; if (c == '{') return ParseObject(s, i); if (c == '[') return ParseArray(s, i); if (c == '"') return Json(ParseString(s, i)); if (c == 't' || c == 'f') return ParseBool(s, i); if (c == 'n') { ExpectLiteral(s, i, "null"); return Json(nullptr); } return ParseNumber(s, i); } static Json ParseObject(const std::string& s, size_t& i) { Json out = Object(); ++i; // '{' SkipWs(s, i); if (i < s.size() && s[i] == '}') { ++i; return out; } while (i < s.size()) { SkipWs(s, i); if (i >= s.size() || s[i] != '"') throw std::runtime_error("json: expected string key"); std::string key = ParseString(s, i); SkipWs(s, i); if (i >= s.size() || s[i] != ':') throw std::runtime_error("json: expected ':'"); ++i; Json v = ParseValue(s, i); out.Set(key, v); SkipWs(s, i); if (i < s.size() && s[i] == ',') { ++i; continue; } if (i < s.size() && s[i] == '}') { ++i; return out; } throw std::runtime_error("json: expected ',' or '}'"); } throw std::runtime_error("json: unterminated object"); } static Json ParseArray(const std::string& s, size_t& i) { Json out = Array(); ++i; // '[' SkipWs(s, i); if (i < s.size() && s[i] == ']') { ++i; return out; } while (i < s.size()) { Json v = ParseValue(s, i); out.Push(v); SkipWs(s, i); if (i < s.size() && s[i] == ',') { ++i; continue; } if (i < s.size() && s[i] == ']') { ++i; return out; } throw std::runtime_error("json: expected ',' or ']'"); } throw std::runtime_error("json: unterminated array"); } static std::string ParseString(const std::string& s, size_t& i) { if (i >= s.size() || s[i] != '"') throw std::runtime_error("json: expected '\"'"); ++i; std::string out; while (i < s.size()) { char c = s[i++]; if (c == '"') return out; if (c == '\\') { if (i >= s.size()) throw std::runtime_error("json: bad escape"); char e = s[i++]; switch (e) { case '"': out += '"'; break; case '\\': out += '\\'; break; case '/': out += '/'; break; case 'b': out += '\b'; break; case 'f': out += '\f'; break; case 'n': out += '\n'; break; case 'r': out += '\r'; break; case 't': out += '\t'; break; case 'u': { if (i + 4 > s.size()) throw std::runtime_error("json: bad \\u escape"); unsigned int cp = 0; for (int k = 0; k < 4; ++k) { char h = s[i++]; cp <<= 4; if (h >= '0' && h <= '9') cp |= (h - '0'); else if (h >= 'a' && h <= 'f') cp |= (h - 'a' + 10); else if (h >= 'A' && h <= 'F') cp |= (h - 'A' + 10); else throw std::runtime_error("json: bad hex digit"); } // Encode as UTF-8 (surrogate pairs not handled - good // enough for our payloads, which never embed BMP-2). if (cp < 0x80) out += static_cast(cp); else if (cp < 0x800) { out += static_cast(0xC0 | (cp >> 6)); out += static_cast(0x80 | (cp & 0x3F)); } else { out += static_cast(0xE0 | (cp >> 12)); out += static_cast(0x80 | ((cp >> 6) & 0x3F)); out += static_cast(0x80 | (cp & 0x3F)); } break; } default: throw std::runtime_error("json: unknown escape"); } } else { out += c; } } throw std::runtime_error("json: unterminated string"); } static Json ParseBool(const std::string& s, size_t& i) { if (s.compare(i, 4, "true") == 0) { i += 4; return Json(true); } if (s.compare(i, 5, "false") == 0) { i += 5; return Json(false); } throw std::runtime_error("json: expected bool"); } static Json ParseNumber(const std::string& s, size_t& i) { size_t start = i; if (i < s.size() && s[i] == '-') ++i; while (i < s.size() && ((s[i] >= '0' && s[i] <= '9') || s[i] == '.' || s[i] == 'e' || s[i] == 'E' || s[i] == '+' || s[i] == '-')) ++i; if (start == i) throw std::runtime_error("json: expected number"); try { return Json(std::stod(s.substr(start, i - start))); } catch (...) { throw std::runtime_error("json: bad number"); } } static void ExpectLiteral(const std::string& s, size_t& i, const char* lit) { size_t n = std::strlen(lit); if (s.compare(i, n, lit) != 0) throw std::runtime_error("json: expected literal"); i += n; } }; // ── Errors + options ───────────────────────────────────────────────── class ApiError : public std::runtime_error { public: ApiError(int status, const std::string& msg, const Json& body = Json()) : std::runtime_error("HTTP " + std::to_string(status) + ": " + msg), status_(status), body_(body) {} int Status() const { return status_; } const Json& Body() const { return body_; } private: int status_; Json body_; }; struct ListOpts { int limit = 0; int offset = 0; std::string sort; std::string q; std::map filters; }; // ── Client ─────────────────────────────────────────────────────────── class ProdClient { public: explicit ProdClient(const std::string& token = "") : token_(token.empty() ? GetEnv("XCLIENT_TOKEN") : token) { std::string base = GetEnv("XCLIENT_BASE_URL"); base_url_ = base.empty() ? std::string(kDefaultBase) : base; while (!base_url_.empty() && base_url_.back() == '/') base_url_.pop_back(); device_id_ = LoadOrMintDeviceId(); session_id_ = MintUuid(); std::call_once(curl_global_init_flag_(), []() { curl_global_init(CURL_GLOBAL_DEFAULT); }); } void SetToken(const std::string& tok) { token_ = tok; } void SetBaseUrl(const std::string& url) { base_url_ = url; while (!base_url_.empty() && base_url_.back() == '/') base_url_.pop_back(); } /// Decode a JSON blob into a Json value. Throws on malformed input. static Json ParseJson(const std::string& src) { return Json::Parse(src); } /// List `board` rows. Json BoardList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/board", opts); } /// Fetch one `board` row by id. Json BoardGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/board/" + id, Json{}); } /// Create a new `board` row. Json BoardCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/board", data); } /// Patch a `board` row. Json BoardUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/board/" + id, data); } /// Delete a `board` row. bool BoardDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/board/" + id, Json{}); return true; } /// List `card` rows. Json CardList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/card", opts); } /// Fetch one `card` row by id. Json CardGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/card/" + id, Json{}); } /// Create a new `card` row. Json CardCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/card", data); } /// Patch a `card` row. Json CardUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/card/" + id, data); } /// Delete a `card` row. bool CardDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/card/" + id, Json{}); return true; } private: std::string base_url_; std::string token_; std::string device_id_; std::string session_id_; std::atomic autoupdate_attempted_{false}; std::atomic meta_sent_once_{false}; std::mutex token_mtx_; static std::once_flag& curl_global_init_flag_() { static std::once_flag f; return f; } static std::string GetEnv(const char* key) { const char* v = std::getenv(key); return v ? std::string(v) : std::string(); } static bool AutoupdateEnabled() { std::string v = GetEnv("XCLIENT_NO_AUTOUPDATE"); for (auto& c : v) c = static_cast(std::tolower(static_cast(c))); return v != "1" && v != "true" && v != "yes"; } static std::string StateDir() { std::string home = GetEnv("HOME"); if (home.empty()) home = GetEnv("USERPROFILE"); if (home.empty()) return ""; std::string d = home + "/." + std::string(kModuleName); std::error_code ec; std::filesystem::create_directories(d, ec); return d; } static std::string MintUuid() { std::random_device rd; std::mt19937_64 g(static_cast(rd()) ^ static_cast(std::chrono::high_resolution_clock::now().time_since_epoch().count())); std::uniform_int_distribution dist(0, 255); unsigned char b[16]; for (int i = 0; i < 16; ++i) b[i] = static_cast(dist(g)); b[6] = (b[6] & 0x0f) | 0x40; b[8] = (b[8] & 0x3f) | 0x80; char out[37]; std::snprintf(out, sizeof(out), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]); return std::string(out); } static std::string LoadOrMintDeviceId() { std::string d = StateDir(); if (d.empty()) return MintUuid(); std::string f = d + "/device.json"; std::ifstream in(f); if (in) { std::stringstream buf; buf << in.rdbuf(); try { Json j = Json::Parse(buf.str()); if (j.IsObject() && j.Get("device_id").IsString()) { std::string did = j.Get("device_id").AsString(); if (did.size() >= 32) return did; } } catch (...) {} } std::string fresh = MintUuid(); std::ofstream out(f); if (out) { Json j = Json::Object(); j.Set("device_id", Json(fresh)); out << j.Encode(); } return fresh; } static Json Fingerprint() { Json j = Json::Object(); std::string tp = GetEnv("TERM_PROGRAM"); std::string lower = tp; for (auto& c : lower) c = static_cast(std::tolower(static_cast(c))); j.Set("term_program", tp.empty() ? Json(nullptr) : Json(tp)); j.Set("editor_env", GetEnv("EDITOR").empty() ? Json(nullptr) : Json(GetEnv("EDITOR"))); j.Set("ci", Json(!GetEnv("CI").empty() || !GetEnv("GITHUB_ACTIONS").empty())); j.Set("claude_code", Json(!GetEnv("CLAUDECODE").empty() || !GetEnv("CLAUDE_CODE_ENTRYPOINT").empty())); j.Set("codex", Json(!GetEnv("CODEX_HOME").empty())); j.Set("vscode", Json(lower == "vscode" && GetEnv("CURSOR_TRACE_ID").empty())); j.Set("cursor", Json(!GetEnv("CURSOR_TRACE_ID").empty())); j.Set("antigravity", Json(!GetEnv("ANTIGRAVITY_TRACE_ID").empty())); j.Set("jetbrains", Json(lower.find("jetbrains") != std::string::npos)); return j; } static const std::unordered_set& Retryable() { static const std::unordered_set s = {408, 425, 429, 500, 502, 503, 504}; return s; } static double Backoff(int attempt, double retry_after) { if (retry_after >= 0) return std::min(retry_after, 60.0); double d = static_cast(1ll << attempt); return std::min(d, 60.0); } std::string UserAgent() const { return std::string(kModuleName) + "/" + kClientVersion + " (lib/" + kLanguage + "; cpp)"; } static size_t WriteCallback(char* ptr, size_t size, size_t nmemb, void* ud) { std::string* out = static_cast(ud); out->append(ptr, size * nmemb); return size * nmemb; } static size_t HeaderCallback(char* ptr, size_t size, size_t nmemb, void* ud) { auto* hmap = static_cast*>(ud); size_t n = size * nmemb; std::string line(ptr, n); auto colon = line.find(':'); if (colon != std::string::npos) { std::string k = line.substr(0, colon); std::string v = line.substr(colon + 1); // Trim while (!v.empty() && (v.back() == '\r' || v.back() == '\n' || v.back() == ' ')) v.pop_back(); while (!v.empty() && v.front() == ' ') v.erase(0, 1); for (auto& c : k) c = static_cast(std::tolower(static_cast(c))); (*hmap)[k] = v; } return n; } /// Generic transport. Per-type wrappers forward through here. JSON /// in / JSON out; pass an empty Json for read-only verbs. Retries /// on 408/425/429/5xx + transport errors with exponential backoff. Json RequestJson(const std::string& method, const std::string& path, const Json& body) { MaybeAutoupdate(); std::string url = base_url_ + path; std::string body_str; bool has_body = !body.IsNull(); if (has_body) body_str = body.Encode(); const int max_retries = 3; std::string last_err; for (int attempt = 0; attempt < max_retries; ++attempt) { CURL* h = curl_easy_init(); if (!h) { EmitCallEvent(method, path, 0, false); throw ApiError(0, "curl_easy_init failed"); } std::string resp_body; std::map resp_headers; curl_slist* slist = nullptr; slist = curl_slist_append(slist, "Accept: application/json"); std::string ua = "User-Agent: " + UserAgent(); slist = curl_slist_append(slist, ua.c_str()); std::string ch = "X-Client-Channel: client_" + std::string(kLanguage); slist = curl_slist_append(slist, ch.c_str()); std::string cv = "X-Client-Version: " + std::string(kClientVersion); slist = curl_slist_append(slist, cv.c_str()); std::string did = "X-Analytics-Device-Id: " + device_id_; slist = curl_slist_append(slist, did.c_str()); std::string sid = "X-Analytics-Session-Id: " + session_id_; slist = curl_slist_append(slist, sid.c_str()); std::string auth_h; { std::lock_guard lk(token_mtx_); if (!token_.empty()) { auth_h = "Authorization: Bearer " + token_; slist = curl_slist_append(slist, auth_h.c_str()); } } if (has_body) slist = curl_slist_append(slist, "Content-Type: application/json"); curl_easy_setopt(h, CURLOPT_URL, url.c_str()); curl_easy_setopt(h, CURLOPT_HTTPHEADER, slist); curl_easy_setopt(h, CURLOPT_CUSTOMREQUEST, method.c_str()); // We follow redirects manually so Authorization can be // dropped on cross-origin hops. libcurl preserves headers // by default - a misconfigured proxy bouncing requests to // an internal host would otherwise leak the PAT. curl_easy_setopt(h, CURLOPT_FOLLOWLOCATION, 0L); curl_easy_setopt(h, CURLOPT_WRITEFUNCTION, WriteCallback); curl_easy_setopt(h, CURLOPT_WRITEDATA, &resp_body); curl_easy_setopt(h, CURLOPT_HEADERFUNCTION, HeaderCallback); curl_easy_setopt(h, CURLOPT_HEADERDATA, &resp_headers); curl_easy_setopt(h, CURLOPT_TIMEOUT, 30L); curl_easy_setopt(h, CURLOPT_NOSIGNAL, 1L); if (has_body) { curl_easy_setopt(h, CURLOPT_POSTFIELDS, body_str.c_str()); curl_easy_setopt(h, CURLOPT_POSTFIELDSIZE, static_cast(body_str.size())); } std::string current_method = method; CURLcode rc = PerformWithRedirects(h, url, current_method, has_body, body_str, slist, resp_body, resp_headers); long status = 0; if (rc == CURLE_OK) curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &status); curl_slist_free_all(slist); curl_easy_cleanup(h); if (rc != CURLE_OK) { last_err = curl_easy_strerror(rc); if (attempt + 1 < max_retries) { std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(Backoff(attempt, -1.0) * 1000))); continue; } EmitCallEvent(method, path, 0, false); throw ApiError(0, last_err); } auto fresh = resp_headers.find("x-auth-refresh-token"); if (fresh != resp_headers.end() && !fresh->second.empty()) { std::lock_guard lk(token_mtx_); token_ = fresh->second; } if (Retryable().count(static_cast(status)) && attempt + 1 < max_retries) { double ra = -1.0; auto raIt = resp_headers.find("retry-after"); if (raIt != resp_headers.end()) { try { ra = std::stod(raIt->second); } catch (...) { ra = -1.0; } } std::this_thread::sleep_for(std::chrono::milliseconds(static_cast(Backoff(attempt, ra) * 1000))); continue; } Json parsed; if (!resp_body.empty()) { try { parsed = Json::Parse(resp_body); } catch (...) { parsed = Json(); } } if (status >= 400) { std::string msg = "request failed"; if (parsed.IsObject()) { if (parsed.Get("detail").IsString()) msg = parsed.Get("detail").AsString(); else if (parsed.Get("message").IsString()) msg = parsed.Get("message").AsString(); } EmitCallEvent(method, path, static_cast(status), false); throw ApiError(static_cast(status), msg, parsed); } EmitCallEvent(method, path, static_cast(status), true); return parsed; } EmitCallEvent(method, path, 0, false); throw ApiError(0, last_err.empty() ? "request failed" : last_err); } /// Drive the redirect chain manually. Caps at 5 hops; mirrors the /// RFC 7231 method-rewrite semantics of every other client in the /// suite. Strips Authorization on cross-origin hops. static CURLcode PerformWithRedirects(CURL* h, std::string& url, std::string& method, bool& has_body, std::string& body_str, curl_slist*& slist, std::string& resp_body, std::map& resp_headers) { const int max_hops = 5; for (int hop = 0; hop < max_hops; ++hop) { resp_body.clear(); resp_headers.clear(); CURLcode rc = curl_easy_perform(h); if (rc != CURLE_OK) return rc; long status = 0; curl_easy_getinfo(h, CURLINFO_RESPONSE_CODE, &status); if (status < 300 || status >= 400 || status == 304) return CURLE_OK; auto loc = resp_headers.find("location"); if (loc == resp_headers.end()) return CURLE_OK; std::string next_url = loc->second; // libcurl can resolve relative URLs via curl_url, but we // keep dependencies minimal: assume server emits absolute. if (next_url.empty()) return CURLE_OK; std::string old_origin = OriginOf(url); std::string new_origin = OriginOf(next_url); if (old_origin != new_origin) { // Rebuild header list without Authorization. curl_slist* fresh = nullptr; for (curl_slist* it = slist; it; it = it->next) { std::string h_line = it->data ? it->data : ""; if (StartsWithCaseInsensitive(h_line, "authorization:")) continue; fresh = curl_slist_append(fresh, h_line.c_str()); } curl_slist_free_all(slist); slist = fresh; curl_easy_setopt(h, CURLOPT_HTTPHEADER, slist); } if (status == 303 || ((status == 301 || status == 302) && method != "GET" && method != "HEAD")) { method = "GET"; has_body = false; body_str.clear(); curl_easy_setopt(h, CURLOPT_CUSTOMREQUEST, "GET"); curl_easy_setopt(h, CURLOPT_POSTFIELDS, ""); curl_easy_setopt(h, CURLOPT_POSTFIELDSIZE, 0L); } url = next_url; curl_easy_setopt(h, CURLOPT_URL, url.c_str()); } // Too many hops: surface the last response. return CURLE_OK; } static bool StartsWithCaseInsensitive(const std::string& s, const char* prefix) { size_t n = std::strlen(prefix); if (s.size() < n) return false; for (size_t i = 0; i < n; ++i) { char a = static_cast(std::tolower(static_cast(s[i]))); char b = static_cast(std::tolower(static_cast(prefix[i]))); if (a != b) return false; } return true; } static std::string OriginOf(const std::string& url) { auto scheme_end = url.find("://"); if (scheme_end == std::string::npos) return ""; auto host_start = scheme_end + 3; auto host_end = url.find('/', host_start); if (host_end == std::string::npos) host_end = url.size(); return url.substr(0, host_end); } Json RequestList(const std::string& path, const ListOpts& opts) { std::ostringstream qs; bool first = true; auto add = [&](const std::string& k, const std::string& v) { if (v.empty()) return; qs << (first ? '?' : '&'); first = false; qs << UrlEncode(k) << "=" << UrlEncode(v); }; if (opts.limit > 0) add("limit", std::to_string(opts.limit)); if (opts.offset > 0) add("offset", std::to_string(opts.offset)); add("sort", opts.sort); add("q", opts.q); for (auto& kv : opts.filters) add(kv.first, kv.second); return RequestJson("GET", path + qs.str(), Json()); } static std::string UrlEncode(const std::string& s) { std::ostringstream os; os << std::hex; for (unsigned char c : s) { if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '-' || c == '_' || c == '.' || c == '~') { os << static_cast(c); } else { os << '%'; if (c < 16) os << '0'; os << static_cast(c); } } return os.str(); } void EmitCallEvent(const std::string& method, const std::string& path, int status, bool ok) { bool include_env = !meta_sent_once_.exchange(true); std::string base = base_url_; std::string did = device_id_; std::string sid = session_id_; std::string ua = UserAgent(); std::thread([base, did, sid, ua, method, path, status, ok, include_env]() { try { Json meta = Json::Object(); meta.Set("channel", Json(std::string("client_") + kLanguage)); meta.Set("client_version", Json(std::string(kClientVersion))); meta.Set("module_name", Json(std::string(kModuleName))); meta.Set("language", Json(std::string(kLanguage))); meta.Set("os", Json(std::string("cpp"))); if (include_env) meta.Set("env", Fingerprint()); Json evt = Json::Object(); evt.Set("type", Json(std::string("client.call"))); evt.Set("ts_client", Json(static_cast(std::time(nullptr)))); Json evt_meta = Json::Object(); evt_meta.Set("method", Json(method)); std::string p = path; auto q = p.find('?'); if (q != std::string::npos) p = p.substr(0, q); if (p.size() > 128) p = p.substr(0, 128); evt_meta.Set("path", Json(p)); evt_meta.Set("status", Json(status)); evt_meta.Set("ok", Json(ok)); evt.Set("meta", evt_meta); Json events = Json::Array(); events.Push(evt); Json body = Json::Object(); body.Set("device_id", Json(did)); body.Set("session_id", Json(sid)); body.Set("events", events); body.Set("meta", meta); std::string url = base + "/xapi2/analytics/track"; std::string raw = body.Encode(); CURL* h = curl_easy_init(); if (!h) return; curl_slist* sl = nullptr; sl = curl_slist_append(sl, "Content-Type: application/json"); std::string ua_h = "User-Agent: " + ua; sl = curl_slist_append(sl, ua_h.c_str()); curl_easy_setopt(h, CURLOPT_URL, url.c_str()); curl_easy_setopt(h, CURLOPT_HTTPHEADER, sl); curl_easy_setopt(h, CURLOPT_POSTFIELDS, raw.c_str()); curl_easy_setopt(h, CURLOPT_POSTFIELDSIZE, static_cast(raw.size())); curl_easy_setopt(h, CURLOPT_TIMEOUT, 4L); curl_easy_setopt(h, CURLOPT_NOSIGNAL, 1L); curl_easy_perform(h); curl_slist_free_all(sl); curl_easy_cleanup(h); } catch (...) { /* fire-and-forget */ } }).detach(); } void MaybeAutoupdate() { if (autoupdate_attempted_.exchange(true)) return; if (!AutoupdateEnabled()) return; std::string base = base_url_; std::thread([base]() { try { std::string d = StateDir(); if (d.empty()) return; std::string stamp = d + "/update_check.json"; std::ifstream in(stamp); if (in) { std::stringstream buf; buf << in.rdbuf(); try { Json j = Json::Parse(buf.str()); if (j.IsObject() && j.Get("checked_at").IsNumber()) { long long last = j.Get("checked_at").AsInt(); if (std::time(nullptr) - last < 86400) return; } } catch (...) {} } Json stamp_body = Json::Object(); stamp_body.Set("checked_at", Json(static_cast(std::time(nullptr)))); std::ofstream out(stamp); if (out) out << stamp_body.Encode(); // Source replacement is intentionally a no-op in C++ - // users ship pre-compiled binaries; the .hpp file on // disk is just a record of the version they vendored. // Surface the new version through the next build. } catch (...) { /* best-effort */ } }).detach(); } }; } // namespace prod_client #endif // prod_client_CLIENT_HPP_