From f276f061a9a66eb7c4ec3c083e2aefc8b449000e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sat, 7 Mar 2026 20:12:35 -0500 Subject: [PATCH] refactor(cses): store API token in cookies, not git credential Problem: the CSES API token was packed into the git credential password field using a tab separator. This leaked session state into the credential layer and broke with helpers that don't differentiate by path (e.g. `cache`). Solution: store the CSES token via `save_platform_cookies`/ `load_platform_cookies`, the same mechanism every other platform uses for session state. Remove all token logic from `git_credential.lua`. Git credential now stores only `{username, password}` uniformly for all platforms. --- lua/cp/git_credential.lua | 41 +++------------------------------------ scrapers/cses.py | 40 +++++++------------------------------- 2 files changed, 10 insertions(+), 71 deletions(-) diff --git a/lua/cp/git_credential.lua b/lua/cp/git_credential.lua index 54c8715..388512a 100644 --- a/lua/cp/git_credential.lua +++ b/lua/cp/git_credential.lua @@ -1,7 +1,6 @@ ---@class cp.Credentials ---@field username string ---@field password string ----@field token? string local M = {} @@ -57,32 +56,6 @@ local function _parse_output(stdout) return result end -local CSES_TOKEN_SEP = '\t' - ----@param platform string ----@param password string ----@return string, string? -local function _decode_password(platform, password) - if platform ~= 'cses' then - return password, nil - end - local pw, token = password:match('^(.-)' .. CSES_TOKEN_SEP .. '(.+)$') - if pw then - return pw, token - end - return password, nil -end - ----@param password string ----@param token? string ----@return string -local function _encode_password(password, token) - if token then - return password .. CSES_TOKEN_SEP .. token - end - return password -end - ---@param platform string ---@return cp.Credentials? function M.get(platform) @@ -104,13 +77,7 @@ function M.get(platform) return nil end - local password, token = _decode_password(platform, parsed.password) - local creds = { username = parsed.username, password = password } - if token then - creds.token = token - end - - return creds + return { username = parsed.username, password = parsed.password } end ---@param platform string @@ -121,8 +88,7 @@ function M.store(platform, creds) return end - local stored_password = _encode_password(creds.password, creds.token) - local input = _build_input(host, { username = creds.username, password = stored_password }) + local input = _build_input(host, { username = creds.username, password = creds.password }) vim.system({ 'git', 'credential', 'approve' }, { stdin = input, text = true }):wait() end @@ -134,8 +100,7 @@ function M.reject(platform, creds) return end - local stored_password = _encode_password(creds.password, creds.token) - local input = _build_input(host, { username = creds.username, password = stored_password }) + local input = _build_input(host, { username = creds.username, password = creds.password }) vim.system({ 'git', 'credential', 'reject' }, { stdin = input, text = true }):wait() end diff --git a/scrapers/cses.py b/scrapers/cses.py index 33fffb5..0b5d8b3 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -8,7 +8,7 @@ from typing import Any import httpx -from .base import BaseScraper, extract_precision +from .base import BaseScraper, extract_precision, load_platform_cookies, save_platform_cookies from .timeouts import HTTP_TIMEOUT from .models import ( ContestListResult, @@ -248,35 +248,20 @@ class CSESScraper(BaseScraper): if not username or not password: return self._login_error("Missing username or password") - token = credentials.get("token") + token = load_platform_cookies("cses") async with httpx.AsyncClient(follow_redirects=True) as client: if token: print(json.dumps({"status": "checking_login"}), flush=True) if await self._check_token(client, token): - return LoginResult( - success=True, - error="", - credentials={ - "username": username, - "password": password, - "token": token, - }, - ) + return LoginResult(success=True, error="") print(json.dumps({"status": "logging_in"}), flush=True) token = await self._web_login(client, username, password) if not token: return self._login_error("bad_credentials") - return LoginResult( - success=True, - error="", - credentials={ - "username": username, - "password": password, - "token": token, - }, - ) + save_platform_cookies("cses", token) + return LoginResult(success=True, error="") async def stream_tests_for_category_async(self, category_id: str) -> None: async with httpx.AsyncClient( @@ -423,7 +408,7 @@ class CSESScraper(BaseScraper): return self._submit_error("Missing credentials. Use :CP login cses") async with httpx.AsyncClient(follow_redirects=True) as client: - token = credentials.get("token") + token = load_platform_cookies("cses") if token: print(json.dumps({"status": "checking_login"}), flush=True) @@ -435,18 +420,7 @@ class CSESScraper(BaseScraper): token = await self._web_login(client, username, password) if not token: return self._submit_error("bad_credentials") - print( - json.dumps( - { - "credentials": { - "username": username, - "password": password, - "token": token, - } - } - ), - flush=True, - ) + save_platform_cookies("cses", token) print(json.dumps({"status": "submitting"}), flush=True)