Replace cookie HTTP verification with butlerd's Profile.List + UseSavedLogin
Some checks failed
Build plugin / build (push) Has been cancelled

Butlerd stores credentials in its SQLite DB after the first password
login. On restart, Profile.List finds saved profiles and
Profile.UseSavedLogin refreshes them — no cookie persistence needed on
our side.
This commit is contained in:
Jose Falanga 2026-08-17 02:19:22 -03:00
parent 1931ae143a
commit d09bd2ae79
3 changed files with 37 additions and 114 deletions

View file

@ -56,7 +56,6 @@ signal app_installed(cave_id: String, success: bool)
signal app_updated(cave_id: String, success: bool)
signal app_uninstalled(cave_id: String, success: bool)
signal launch_exited(cave_id: String)
signal login_cookies_saved(username: String, cookie: String)
var proc: InteractiveProcess
var socket: StreamPeerTCP
@ -385,109 +384,41 @@ func _login_with_password(username: String, password: String) -> void:
return
profile = res.get("profile", {})
is_logged_in = true
# Persist the cookie so we can re-authenticate on restart. Butlerd
# stores tokens in its own DB, but we re-send the cookie on subsequent
# startups so butlerd can refresh the session if needed.
var cookie_dict: Dictionary = res.get("cookie", {})
if not cookie_dict.is_empty():
var cookie_json := JSON.stringify(cookie_dict)
emit_signal.call_deferred("login_cookies_saved", username, cookie_json)
emit_signal.call_deferred("logged_in", LOGIN_STATUS.OK, profile)
## Re-authenticates using a previously saved cookie. The cookie is a JSON
## dict of name-value pairs from Profile.LoginWithPassword. We send it to
## itch.io's API directly to verify the session — butlerd doesn't expose a
## "login with cookie" method, but it stores the credentials internally so
## subsequent butlerd calls will work once we've verified the session.
func login_with_cookie(cookie_json: String) -> void:
await thread_group.exec(_login_with_cookie.bind(cookie_json))
## Attempts to resume a previous login using butlerd's stored credentials.
## Returns true on success (profile is set, logged_in signal emitted).
func try_saved_login() -> bool:
return await thread_group.exec(_try_saved_login)
func _login_with_cookie(cookie_json: String) -> void:
var cookie_dict: Dictionary = {}
var json := JSON.new()
if json.parse(cookie_json) == OK and typeof(json.data) == TYPE_DICTIONARY:
cookie_dict = json.data
if cookie_dict.is_empty():
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
# Build Cookie header from the dict: "name1=value1; name2=value2"
var pairs: PackedStringArray = []
for key in cookie_dict:
pairs.append(str(key) + "=" + str(cookie_dict[key]))
var cookie_header := "; ".join(pairs)
var http := HTTPClient.new()
var err := http.connect_to_host("api.itch.io", 443, TLSOptions.client())
if err != OK:
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
var deadline := Time.get_ticks_msec() + 15000
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
http.poll()
if Time.get_ticks_msec() > deadline:
http.close()
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
OS.delay_msec(50)
if http.get_status() != HTTPClient.STATUS_CONNECTED:
http.close()
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
var headers := PackedStringArray([
"Cookie: " + cookie_header,
"User-Agent: opencode-itch-plugin/1.0",
])
http.request(HTTPClient.METHOD_GET, "/profile", headers)
deadline = Time.get_ticks_msec() + 15000
while http.get_status() == HTTPClient.STATUS_REQUESTING:
http.poll()
if Time.get_ticks_msec() > deadline:
http.close()
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
OS.delay_msec(10)
var body := PackedByteArray()
while http.get_status() == HTTPClient.STATUS_BODY:
http.poll()
var chunk: PackedByteArray = http.read_response_body_chunk()
if chunk.is_empty():
if Time.get_ticks_msec() > deadline:
http.close()
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
OS.delay_msec(10)
continue
body.append_array(chunk)
http.close()
var response_code := http.get_response_code()
var response_text := body.get_string_from_utf8()
if response_code != 200 or response_text.is_empty():
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
var resp_json := JSON.new()
if resp_json.parse(response_text) != OK:
is_logged_in = false
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
return
var data: Dictionary = resp_json.data
profile = data.get("user", data)
func _try_saved_login() -> bool:
var list_res := await _rpc_call("Profile.List", {})
if "error" in list_res:
return false
var profiles: Array = list_res.get("profiles", [])
if profiles.is_empty():
return false
# Use the most recently connected profile.
var best: Dictionary = profiles[0]
var best_time: int = int(best.get("lastConnected", 0))
for p in profiles:
var t: int = int(p.get("lastConnected", 0))
if t > best_time:
best = p
best_time = t
var profile_id: int = int(best.get("id", 0))
if profile_id == 0:
return false
var use_res := await _rpc_call("Profile.UseSavedLogin", {"profileId": profile_id})
if "error" in use_res:
logger.warn("Profile.UseSavedLogin failed: " + str(use_res["error"]))
return false
profile = use_res.get("profile", {})
is_logged_in = true
emit_signal.call_deferred("logged_in", LOGIN_STATUS.OK, profile)
return true
## Returns every game the logged-in profile owns a download key for.