--- jasper/auth.lua --- Token management, shared with the jasper_waybar.py Python script. --- Token cache: $XDG_CONFIG_HOME/jasper/token.json (same path as Python) local M = {} local constants = require("jasper.constants") local JASPER_URL = constants.JASPER_URL --- @return string path to the token cache file local function token_file_path() local xdg = os.getenv("XDG_CONFIG_HOME") or (os.getenv("HOME") .. "/.config") return xdg .. "/jasper/token.json" end --- Read the cached auth token from disk. --- @return string|nil function M.get_token() local path = token_file_path() if vim.fn.filereadable(path) ~= 1 then return nil end local lines = vim.fn.readfile(path) if not lines or #lines == 0 then return nil end local ok, data = pcall(vim.fn.json_decode, table.concat(lines, "\n")) if ok and type(data) == "table" and data.auth_token then return data.auth_token end return nil end --- Delete the cached token (e.g. on expiry). function M.delete_token() local path = token_file_path() if vim.fn.filereadable(path) == 1 then vim.fn.delete(path) end end --- Perform a login request and cache the resulting token. --- @param username string --- @param password string --- @return string|nil token --- @return string|nil error message function M.login(username, password) -- Write credentials to a temp curl config file so they never appear in -- the process argument list (ps aux / /proc//cmdline). local tmpfile = os.tmpname() local cf = io.open(tmpfile, "w") if not cf then return nil, "cannot create temp file for curl config" end cf:write('--data-urlencode "username=' .. username .. '"\n') cf:write('--data-urlencode "password=' .. password .. '"\n') cf:close() local result = vim.system({ "curl", "-s", "-X", "POST", "--config", tmpfile, JASPER_URL .. "/api/v1/token/login/", }, { text = true }):wait() os.remove(tmpfile) if result.code ~= 0 then return nil, "curl error (exit " .. result.code .. ")" end local ok, data = pcall(vim.fn.json_decode, result.stdout) if not ok or type(data) ~= "table" or not data.auth_token then return nil, "login failed (bad credentials or unexpected response)" end -- Save to cache (same format as Python script) local path = token_file_path() local dir = vim.fn.fnamemodify(path, ":h") vim.fn.mkdir(dir, "p") vim.fn.writefile({ vim.fn.json_encode(data) }, path) return data.auth_token, nil end --- Try to retrieve a password from the system Secret Service via secret-tool. --- @param username string --- @return string|nil password local function secret_tool_lookup(username) local result = vim.system({ "secret-tool", "lookup", "service", "jasper", "user", username }, { text = true }) :wait() if result.code == 0 and result.stdout and result.stdout ~= "" then -- strip trailing newline return (result.stdout:gsub("%s+$", "")) end return nil end --- Attempt login with the given username: try Secret Service first, then prompt. --- @param username string --- @param callback fun(token: string|nil) local function login_with_username(username, callback) local password = secret_tool_lookup(username) if password then local token, err = M.login(username, password) if token and not err then vim.notify("[Jasper] Login successful (Secret Service)", vim.log.levels.INFO) callback(token) return end -- Secret Service password was wrong or login failed — fall through to prompt vim.notify("[Jasper] Secret Service password failed, prompting", vim.log.levels.WARN) end -- Prompt for password local pwd = vim.fn.inputsecret("Jasper password: ") if not pwd or pwd == "" then vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN) callback(nil) return end local token, err = M.login(username, pwd) if err or not token then vim.notify("[Jasper] " .. (err or "Login failed"), vim.log.levels.ERROR) callback(nil) return end vim.notify("[Jasper] Login successful", vim.log.levels.INFO) callback(token) end --- Interactively ask the user for credentials using vim.ui.input, --- then call login(). Calls `callback(token)` on success, `callback(nil)` on failure. --- Checks JASPER_USERNAME env var and Secret Service before prompting. --- @param callback fun(token: string|nil) function M.prompt_login(callback) local env_username = os.getenv("JASPER_USERNAME") if env_username and env_username ~= "" then login_with_username(env_username, callback) else vim.ui.input({ prompt = "Jasper username: " }, function(username) if not username or username == "" then vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN) callback(nil) return end login_with_username(username, callback) end) end end return M