feat(auth): add Secret Service and env var support for login

- Introduce `secret_tool_lookup` to retrieve passwords via `secret-tool`
- Extract `login_with_username` to try Secret Service before prompting
- Read `JASPER_USERNAME` env var to skip username prompt when set
- Fall back to `inputsecret` prompt if Secret Service lookup fails
This commit is contained in:
2026-05-02 17:39:52 +02:00
parent 4a5acb17be
commit 53fbabe33e

View File

@@ -88,24 +88,42 @@ function M.login(username, password)
return data.auth_token, nil return data.auth_token, nil
end end
--- Interactively ask the user for credentials using vim.ui.input, --- Try to retrieve a password from the system Secret Service via secret-tool.
--- then call login(). Calls `callback(token)` on success, `callback(nil)` on failure. --- @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) --- @param callback fun(token: string|nil)
function M.prompt_login(callback) local function login_with_username(username, callback)
vim.ui.input({ prompt = "Jasper username: " }, function(username) local password = secret_tool_lookup(username)
if not username or username == "" then if password then
vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN)
callback(nil)
return
end
-- vim.fn.inputsecret() is native Neovim: hides typed characters in all environments
local password = vim.fn.inputsecret("Jasper password: ")
if not password or password == "" then
vim.notify("[Jasper] Login cancelled", vim.log.levels.WARN)
callback(nil)
return
end
local token, err = M.login(username, password) 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 if err or not token then
vim.notify("[Jasper] " .. (err or "Login failed"), vim.log.levels.ERROR) vim.notify("[Jasper] " .. (err or "Login failed"), vim.log.levels.ERROR)
callback(nil) callback(nil)
@@ -113,7 +131,26 @@ function M.prompt_login(callback)
end end
vim.notify("[Jasper] Login successful", vim.log.levels.INFO) vim.notify("[Jasper] Login successful", vim.log.levels.INFO)
callback(token) 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
end end
return M return M