Files
jasper.nvim/lua/jasper/auth.lua
Paolo Donadeo 9c122a312f feat(security,timer): harden auth and add multi-instance coordination
- Extract JASPER_URL into a shared constants module
- Pass login credentials via a temp curl config file to avoid exposure
  in the process argument list (ps/proc)
- Replace vim.ui.input secret prompt with vim.fn.inputsecret()
- Add -s (silent) flag to all curl calls to suppress progress output
- Guard curl output parser against missing newline in stdout
- Track per-activity shared timestamp file
  (/tmp/jasper_<id>.last_activity)
  so the inactivity watchdog skips auto-pause when another Neovim
  instance is still active on the same task
- Clean up leftover uv_timer on repeated begin_tracking calls
- Remove shared activity file on teardown only when this instance wrote
  it
2026-04-18 19:17:44 +02:00

120 lines
3.5 KiB
Lua

--- 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/<pid>/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
--- Interactively ask the user for credentials using vim.ui.input,
--- then call login(). Calls `callback(token)` on success, `callback(nil)` on failure.
--- @param callback fun(token: string|nil)
function M.prompt_login(callback)
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
-- 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)
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)
end
return M