// Drop-in C++ client library for the KI BMS HTTP API. // // Save this file alongside your source as `AtsClient.hpp` and use // the AtsClient class: // // #include "AtsClient.hpp" // ats_client::AtsClient 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 AtsClient. 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 ats_client_CLIENT_HPP_ #define ats_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 ats_client { // ── Identity (substituted at generation time) ──────────────────────── inline constexpr const char* kAppSlug = "ats"; inline constexpr const char* kAppName = "KI BMS"; inline constexpr const char* kModuleName = "ats_client"; inline constexpr const char* kClientVersion = "0.3.13"; inline constexpr const char* kLanguage = "cpp"; inline constexpr const char* kDefaultBase = "https://www.ki-bewerber-management.de"; // 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 // AtsClient::ParseJson. inline const char* TypesJson() { static const char* kBlob = R"JSON_BLOB({"application":{"ops":["list","read","create","update","delete"],"create_fields":["job_id","candidate_id","stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"update_fields":["stage","previous_stage","position","applied_at","last_stage_at","source_id","source_label","cover_letter","cv_blob_id","cv_url","answers","fit_score","fit_reasoning","fit_flags","fit_computed_at","rejected_reason","rejected_note","tags"],"allowed_filters":["data__job_id","data__candidate_id","data__stage","data__source_id","data__rejected_reason","data__is_archived","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__applied_at","data__fit_score","data__position","data__last_stage_at"],"default_sort":"data__position","max_limit":500,"fields":[{"name":"tags","type":"tags"},{"name":"stage","type":"enum","values":["new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"cv_url","type":"url","max_len":2048},{"name":"job_id","type":"string","max_len":64,"ref":{"type":"job","owned":false,"optional":false}},{"name":"answers","type":"list"},{"name":"position","type":"number"},{"name":"fit_flags","type":"tags"},{"name":"fit_score","type":"number"},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"applied_at","type":"string","max_len":32},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"candidate_id","type":"string","max_len":64,"ref":{"type":"candidate","owned":false,"optional":false}},{"name":"cover_letter","type":"string","max_len":16000},{"name":"source_label","type":"string","max_len":200},{"name":"fit_reasoning","type":"string","max_len":4000},{"name":"last_stage_at","type":"string","max_len":32},{"name":"rejected_note","type":"string","max_len":2000},{"name":"previous_stage","type":"string","max_len":32},{"name":"fit_computed_at","type":"string","max_len":32},{"name":"rejected_reason","type":"enum","values":["not_qualified","salary_mismatch","location_mismatch","culture_mismatch","withdrew","ghosted","filled_internally","duplicate","other"]}]},"application_note":{"ops":["list","read","create","update","delete"],"create_fields":["body","pinned","private","parent_kind","parent_id"],"update_fields":["body","pinned","private"],"allowed_filters":["data__parent_id","data__parent_kind","data__pinned","data__private","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":8000},{"name":"pinned","type":"bool"},{"name":"private","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]}]},"candidate":{"ops":["list","read","create","update","delete"],"create_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"update_fields":["name","first_name","last_name","salutation","pronouns","email","phone","city","country","current_company","current_role","years_experience","available_from","salary_expectation","currency","linkedin","github","portfolio","cv_url","cv_blob_id","avatar_blob_id","summary","skills","languages","tags","source_id","source_label","pool_status","gdpr_consent","gdpr_consent_at","gdpr_retention_until","preferred_locale","last_touched_at","color"],"allowed_filters":["data__email","data__name","data__location","data__country","data__source_id","data__tags","data__skills","data__pool_status","data__gdpr_consent","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__name","data__last_touched_at"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"city","type":"string","max_len":120},{"name":"name","type":"string","max_len":200},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"email","type":"string","max_len":320},{"name":"phone","type":"string","max_len":64},{"name":"cv_url","type":"url","max_len":2048},{"name":"github","type":"url","max_len":2048},{"name":"skills","type":"tags"},{"name":"country","type":"string","max_len":120},{"name":"summary","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"linkedin","type":"url","max_len":2048},{"name":"pronouns","type":"string","max_len":32},{"name":"languages","type":"tags"},{"name":"last_name","type":"string","max_len":120},{"name":"portfolio","type":"url","max_len":2048},{"name":"source_id","type":"string","max_len":64,"ref":{"type":"source","owned":false,"optional":false}},{"name":"cv_blob_id","type":"string","max_len":64},{"name":"first_name","type":"string","max_len":120},{"name":"salutation","type":"enum","values":["herr","frau","divers","neutral"]},{"name":"pool_status","type":"enum","values":["active","talent_pool","blocked","withdrawn"]},{"name":"current_role","type":"string","max_len":200},{"name":"gdpr_consent","type":"bool"},{"name":"source_label","type":"string","max_len":200},{"name":"available_from","type":"string","max_len":32},{"name":"avatar_blob_id","type":"string","max_len":64},{"name":"current_company","type":"string","max_len":200},{"name":"gdpr_consent_at","type":"string","max_len":32},{"name":"last_touched_at","type":"string","max_len":32},{"name":"preferred_locale","type":"string","max_len":16},{"name":"years_experience","type":"number"},{"name":"salary_expectation","type":"number"},{"name":"gdpr_retention_until","type":"string","max_len":32}]},"email_template":{"ops":["list","read","create","update","delete"],"create_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"update_fields":["name","category","subject","body","language","stage_trigger","auto_send","active","variables_doc"],"allowed_filters":["data__name","data__category","data__stage_trigger","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"name","type":"string","max_len":200},{"name":"active","type":"bool"},{"name":"subject","type":"string","max_len":400},{"name":"category","type":"enum","values":["acknowledge","screening_invite","interview_invite","rejection","offer","talent_pool","other"]},{"name":"language","type":"string","max_len":16},{"name":"auto_send","type":"bool"},{"name":"stage_trigger","type":"enum","values":["","new","review","screening","interview","offer","hired","rejected","talent_pool"]},{"name":"variables_doc","type":"string","max_len":2000}]},"evaluation":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","interview_id","interviewer_id","skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"update_fields":["skills_score","culture_score","communication_score","potential_score","overall_score","recommendation","highlights","concerns","summary"],"allowed_filters":["data__application_id","data__interview_id","data__interviewer_id","data__recommendation","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__overall_score"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"summary","type":"string","max_len":4000},{"name":"concerns","type":"string","max_len":4000},{"name":"highlights","type":"string","max_len":4000},{"name":"interview_id","type":"string","max_len":64,"ref":{"type":"interview","owned":false,"optional":false}},{"name":"skills_score","type":"number"},{"name":"culture_score","type":"number"},{"name":"overall_score","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"recommendation","type":"enum","values":["strong_yes","yes","neutral","no","strong_no"]},{"name":"potential_score","type":"number"},{"name":"communication_score","type":"number"}]},"interview":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"update_fields":["kind","status","title","scheduled_at","duration_minutes","location","meeting_url","interviewer_id","interviewer_ids","agenda","notes","send_invite"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__kind","data__status","data__interviewer_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__scheduled_at","created_at","updated_at"],"default_sort":"data__scheduled_at","max_limit":200,"fields":[{"name":"kind","type":"enum","values":["phone","video","onsite","take_home","panel","trial_day"]},{"name":"notes","type":"string","max_len":8000},{"name":"title","type":"string","max_len":200},{"name":"agenda","type":"string","max_len":4000},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["scheduled","completed","no_show","cancelled","rescheduled"]},{"name":"location","type":"string","max_len":200},{"name":"meeting_url","type":"url","max_len":2048},{"name":"send_invite","type":"bool"},{"name":"candidate_id","type":"string","max_len":64},{"name":"scheduled_at","type":"string","max_len":32},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"interviewer_id","type":"string","max_len":64},{"name":"interviewer_ids","type":"list"},{"name":"duration_minutes","type":"number"}]},"job":{"ops":["list","read","create","update","delete"],"create_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"update_fields":["title","slug","department","location","country","remote","employment_type","seniority","headcount","salary_min","salary_max","currency","salary_visibility","summary","description","responsibilities","requirements","nice_to_have","benefits","language","status","public","ai_screen_enabled","ai_screen_prompt","knockout_questions","screening_questions","tags","hiring_manager_id","team_ids","opened_at","target_close_date","closed_at","external_apply_url","color"],"allowed_filters":["data__title","data__department","data__location","data__employment_type","data__seniority","data__remote","data__status","data__public","data__hiring_manager_id","data__tags","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","updated_at","data__title","data__opened_at","data__target_close_date"],"default_sort":"created_at","max_limit":200,"fields":[{"name":"slug","type":"string","max_len":120},{"name":"tags","type":"tags"},{"name":"color","type":"string","max_len":24},{"name":"title","type":"string","max_len":200},{"name":"public","type":"bool"},{"name":"remote","type":"enum","values":["onsite","hybrid","remote"]},{"name":"status","type":"enum","values":["draft","open","paused","closed","filled"]},{"name":"country","type":"string","max_len":80},{"name":"summary","type":"string","max_len":600},{"name":"benefits","type":"string","max_len":4000},{"name":"currency","type":"string","max_len":8},{"name":"language","type":"string","max_len":16},{"name":"location","type":"string","max_len":120},{"name":"team_ids","type":"list"},{"name":"closed_at","type":"string","max_len":32},{"name":"headcount","type":"number"},{"name":"opened_at","type":"string","max_len":32},{"name":"seniority","type":"enum","values":["junior","mid","senior","lead","principal"]},{"name":"department","type":"string","max_len":120},{"name":"salary_max","type":"number"},{"name":"salary_min","type":"number"},{"name":"description","type":"string","max_len":16000},{"name":"nice_to_have","type":"string","max_len":4000},{"name":"requirements","type":"string","max_len":8000},{"name":"employment_type","type":"enum","values":["full_time","part_time","internship","working_student","freelance","contract"]},{"name":"ai_screen_prompt","type":"string","max_len":4000},{"name":"responsibilities","type":"string","max_len":8000},{"name":"ai_screen_enabled","type":"bool"},{"name":"hiring_manager_id","type":"string","max_len":64},{"name":"salary_visibility","type":"enum","values":["public","team","private"]},{"name":"target_close_date","type":"string","max_len":32},{"name":"external_apply_url","type":"url","max_len":2048},{"name":"knockout_questions","type":"list"},{"name":"screening_questions","type":"list"}]},"message":{"ops":["list","read","create","update","delete"],"create_fields":["candidate_id","application_id","channel","direction","subject","body","status","sent_at","delivered_at","read_at","template_id","from_address","to_address","cc_addresses","thread_id","error"],"update_fields":["subject","body","status","sent_at","delivered_at","read_at","thread_id","error"],"allowed_filters":["data__candidate_id","data__application_id","data__channel","data__status","data__template_id","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__sent_at"],"default_sort":"-data__sent_at","max_limit":200,"fields":[{"name":"body","type":"string","max_len":16000},{"name":"error","type":"string","max_len":600},{"name":"status","type":"enum","values":["draft","queued","sent","delivered","failed","bounced"]},{"name":"channel","type":"enum","values":["email","note"]},{"name":"read_at","type":"string","max_len":32},{"name":"sent_at","type":"string","max_len":32},{"name":"subject","type":"string","max_len":400},{"name":"direction","type":"enum","values":["outbound","inbound"]},{"name":"thread_id","type":"string","max_len":200},{"name":"to_address","type":"string","max_len":320},{"name":"template_id","type":"string","max_len":64,"ref":{"type":"email_template","owned":false,"optional":false}},{"name":"candidate_id","type":"string","max_len":64},{"name":"cc_addresses","type":"list"},{"name":"delivered_at","type":"string","max_len":32},{"name":"from_address","type":"string","max_len":320},{"name":"application_id","type":"string","max_len":64}]},"offer":{"ops":["list","read","create","update","delete"],"create_fields":["application_id","candidate_id","job_id","salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"update_fields":["salary_gross","salary_period","currency","bonus","bonus_note","vacation_days","start_date","expires_at","term","term_until","weekly_hours","remote_policy","status","sent_at","decided_at","letter_body","letter_blob_id","decline_reason"],"allowed_filters":["data__application_id","data__candidate_id","data__job_id","data__status","data__currency","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__start_date","data__sent_at"],"default_sort":"created_at","max_limit":100,"fields":[{"name":"term","type":"enum","values":["permanent","fixed_term","trial","intern","freelance"]},{"name":"bonus","type":"number"},{"name":"job_id","type":"string","max_len":64},{"name":"status","type":"enum","values":["draft","sent","accepted","declined","withdrawn","expired"]},{"name":"sent_at","type":"string","max_len":32},{"name":"currency","type":"string","max_len":8},{"name":"bonus_note","type":"string","max_len":600},{"name":"decided_at","type":"string","max_len":32},{"name":"expires_at","type":"string","max_len":32},{"name":"start_date","type":"string","max_len":32},{"name":"term_until","type":"string","max_len":32},{"name":"letter_body","type":"string","max_len":16000},{"name":"candidate_id","type":"string","max_len":64},{"name":"salary_gross","type":"number"},{"name":"weekly_hours","type":"number"},{"name":"remote_policy","type":"string","max_len":200},{"name":"salary_period","type":"enum","values":["yearly","monthly","daily","hourly"]},{"name":"vacation_days","type":"number"},{"name":"application_id","type":"string","max_len":64,"ref":{"type":"application","owned":false,"optional":false}},{"name":"decline_reason","type":"string","max_len":600},{"name":"letter_blob_id","type":"string","max_len":64}]},"source":{"ops":["list","read","create","update","delete"],"create_fields":["name","kind","url","active","notes"],"update_fields":["name","kind","url","active","notes"],"allowed_filters":["data__name","data__kind","data__active","status","is_archived","owned_by","created_by"],"allowed_sorts":["created_at","data__name"],"default_sort":"data__name","max_limit":100,"fields":[{"name":"url","type":"url","max_len":2048},{"name":"kind","type":"enum","values":["linkedin","indeed","stepstone","xing","honeypot","kununu","careers_page","referral","active_sourcing","agency","event","other"]},{"name":"name","type":"string","max_len":200},{"name":"notes","type":"string","max_len":2000},{"name":"active","type":"bool"}]},"task":{"ops":["list","read","create","update","delete"],"create_fields":["title","description","due_date","completed","completed_at","priority","assigned_to","parent_kind","parent_id"],"update_fields":["title","description","due_date","completed","completed_at","priority","assigned_to"],"allowed_filters":["data__parent_id","data__parent_kind","data__assigned_to","data__completed","data__priority","status","is_archived","owned_by","created_by"],"allowed_sorts":["data__due_date","created_at","data__priority"],"default_sort":"data__due_date","max_limit":200,"fields":[{"name":"title","type":"string","max_len":200},{"name":"due_date","type":"string","max_len":32},{"name":"priority","type":"enum","values":["low","normal","high","urgent"]},{"name":"completed","type":"bool"},{"name":"parent_id","type":"string","max_len":64},{"name":"assigned_to","type":"string","max_len":64},{"name":"description","type":"string","max_len":4000},{"name":"parent_kind","type":"enum","values":["candidate","application","job"]},{"name":"completed_at","type":"string","max_len":32}]}})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 AtsClient { public: explicit AtsClient(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 `application` rows. Json ApplicationList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/application", opts); } /// Fetch one `application` row by id. Json ApplicationGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/application/" + id, Json{}); } /// Create a new `application` row. Json ApplicationCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/application", data); } /// Patch a `application` row. Json ApplicationUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/application/" + id, data); } /// Delete a `application` row. bool ApplicationDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/application/" + id, Json{}); return true; } /// List `application_note` rows. Json ApplicationNoteList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/application_note", opts); } /// Fetch one `application_note` row by id. Json ApplicationNoteGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/application_note/" + id, Json{}); } /// Create a new `application_note` row. Json ApplicationNoteCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/application_note", data); } /// Patch a `application_note` row. Json ApplicationNoteUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/application_note/" + id, data); } /// Delete a `application_note` row. bool ApplicationNoteDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/application_note/" + id, Json{}); return true; } /// List `candidate` rows. Json CandidateList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/candidate", opts); } /// Fetch one `candidate` row by id. Json CandidateGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/candidate/" + id, Json{}); } /// Create a new `candidate` row. Json CandidateCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/candidate", data); } /// Patch a `candidate` row. Json CandidateUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/candidate/" + id, data); } /// Delete a `candidate` row. bool CandidateDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/candidate/" + id, Json{}); return true; } /// List `email_template` rows. Json EmailTemplateList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/email_template", opts); } /// Fetch one `email_template` row by id. Json EmailTemplateGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/email_template/" + id, Json{}); } /// Create a new `email_template` row. Json EmailTemplateCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/email_template", data); } /// Patch a `email_template` row. Json EmailTemplateUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/email_template/" + id, data); } /// Delete a `email_template` row. bool EmailTemplateDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/email_template/" + id, Json{}); return true; } /// List `evaluation` rows. Json EvaluationList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/evaluation", opts); } /// Fetch one `evaluation` row by id. Json EvaluationGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/evaluation/" + id, Json{}); } /// Create a new `evaluation` row. Json EvaluationCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/evaluation", data); } /// Patch a `evaluation` row. Json EvaluationUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/evaluation/" + id, data); } /// Delete a `evaluation` row. bool EvaluationDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/evaluation/" + id, Json{}); return true; } /// List `interview` rows. Json InterviewList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/interview", opts); } /// Fetch one `interview` row by id. Json InterviewGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/interview/" + id, Json{}); } /// Create a new `interview` row. Json InterviewCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/interview", data); } /// Patch a `interview` row. Json InterviewUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/interview/" + id, data); } /// Delete a `interview` row. bool InterviewDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/interview/" + id, Json{}); return true; } /// List `job` rows. Json JobList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/job", opts); } /// Fetch one `job` row by id. Json JobGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/job/" + id, Json{}); } /// Create a new `job` row. Json JobCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/job", data); } /// Patch a `job` row. Json JobUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/job/" + id, data); } /// Delete a `job` row. bool JobDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/job/" + id, Json{}); return true; } /// List `message` rows. Json MessageList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/message", opts); } /// Fetch one `message` row by id. Json MessageGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/message/" + id, Json{}); } /// Create a new `message` row. Json MessageCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/message", data); } /// Patch a `message` row. Json MessageUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/message/" + id, data); } /// Delete a `message` row. bool MessageDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/message/" + id, Json{}); return true; } /// List `offer` rows. Json OfferList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/offer", opts); } /// Fetch one `offer` row by id. Json OfferGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/offer/" + id, Json{}); } /// Create a new `offer` row. Json OfferCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/offer", data); } /// Patch a `offer` row. Json OfferUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/offer/" + id, data); } /// Delete a `offer` row. bool OfferDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/offer/" + id, Json{}); return true; } /// List `source` rows. Json SourceList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/source", opts); } /// Fetch one `source` row by id. Json SourceGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/source/" + id, Json{}); } /// Create a new `source` row. Json SourceCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/source", data); } /// Patch a `source` row. Json SourceUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/source/" + id, data); } /// Delete a `source` row. bool SourceDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/source/" + id, Json{}); return true; } /// List `task` rows. Json TaskList(const ListOpts& opts = {}) { return RequestList("/xapi2/data/task", opts); } /// Fetch one `task` row by id. Json TaskGet(const std::string& id) { return RequestJson("GET", "/xapi2/data/task/" + id, Json{}); } /// Create a new `task` row. Json TaskCreate(const Json& data) { return RequestJson("POST", "/xapi2/data/task", data); } /// Patch a `task` row. Json TaskUpdate(const std::string& id, const Json& data) { return RequestJson("PATCH", "/xapi2/data/task/" + id, data); } /// Delete a `task` row. bool TaskDelete(const std::string& id) { RequestJson("DELETE", "/xapi2/data/task/" + 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/challenge"; 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 ats_client #endif // ats_client_CLIENT_HPP_