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
This commit is contained in:
@@ -14,10 +14,81 @@ local state = {
|
||||
running = false,
|
||||
-- inactivity_ms: milliseconds of silence before the timer is auto-paused
|
||||
inactivity_ms = 10 * 60 * 1000,
|
||||
-- last epoch (seconds) this instance wrote to the shared activity file
|
||||
last_activity_time = nil, ---@type number|nil
|
||||
uv_timer = nil, ---@type uv.uv_timer_t|nil
|
||||
augroup = nil, ---@type number|nil
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Shared-activity timestamp (multi-instance coordination)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Path of the shared last-activity file for the current attivita_id.
|
||||
--- @return string|nil
|
||||
local function activity_file_path()
|
||||
if not state.attivita_id then
|
||||
return nil
|
||||
end
|
||||
return "/tmp/jasper_" .. tostring(state.attivita_id) .. ".last_activity"
|
||||
end
|
||||
|
||||
--- Write the current epoch (seconds) to the shared activity file.
|
||||
local function touch_activity_file()
|
||||
local path = activity_file_path()
|
||||
if not path then
|
||||
return
|
||||
end
|
||||
local now = os.time()
|
||||
local f = io.open(path, "w")
|
||||
if f then
|
||||
f:write(tostring(now))
|
||||
f:close()
|
||||
state.last_activity_time = now
|
||||
end
|
||||
end
|
||||
|
||||
--- Return true if another instance has recorded activity within `inactivity_ms`.
|
||||
local function other_instance_active()
|
||||
local path = activity_file_path()
|
||||
if not path then
|
||||
return false
|
||||
end
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
return false
|
||||
end
|
||||
local raw = f:read("*l")
|
||||
f:close()
|
||||
local ts = tonumber(raw)
|
||||
if not ts then
|
||||
return false
|
||||
end
|
||||
local elapsed_ms = (os.time() - ts) * 1000
|
||||
return elapsed_ms < state.inactivity_ms
|
||||
end
|
||||
|
||||
--- Remove the shared activity file on exit, but only if this instance was
|
||||
--- the last to write it. If another instance wrote a newer timestamp, leave
|
||||
--- the file intact so the other instance's watchdog keeps working correctly.
|
||||
local function cleanup_activity_file()
|
||||
local path = activity_file_path()
|
||||
if not path or not state.last_activity_time then
|
||||
return
|
||||
end
|
||||
local f = io.open(path, "r")
|
||||
if not f then
|
||||
return
|
||||
end
|
||||
local raw = f:read("*l")
|
||||
f:close()
|
||||
local ts = tonumber(raw)
|
||||
-- Only remove if the timestamp on disk matches what we wrote last
|
||||
if ts and ts == state.last_activity_time then
|
||||
os.remove(path)
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -47,6 +118,12 @@ local function arm_inactivity_watchdog()
|
||||
if not state.running then
|
||||
return
|
||||
end
|
||||
-- Don't pause if another instance with the same attivita_id is still active
|
||||
if other_instance_active() then
|
||||
notify("Inactivity detected, but another instance is active — keeping timer running")
|
||||
arm_inactivity_watchdog()
|
||||
return
|
||||
end
|
||||
local ok, err = api.stop_timer(state.token, state.timer_id)
|
||||
if ok then
|
||||
state.running = false
|
||||
@@ -104,6 +181,7 @@ function M.on_activity()
|
||||
if not state.timer_id then
|
||||
return
|
||||
end
|
||||
touch_activity_file()
|
||||
|
||||
if not state.running then
|
||||
-- Resume after an inactivity pause
|
||||
@@ -131,6 +209,13 @@ local function begin_tracking(token, timer_id)
|
||||
state.token = token
|
||||
state.timer_id = timer_id
|
||||
|
||||
-- Close any leftover uv_timer from a previous (unexpected) call
|
||||
if state.uv_timer then
|
||||
state.uv_timer:stop()
|
||||
state.uv_timer:close()
|
||||
state.uv_timer = nil
|
||||
end
|
||||
|
||||
-- Create the libuv timer used for inactivity detection
|
||||
state.uv_timer = vim.uv.new_timer()
|
||||
|
||||
@@ -209,6 +294,7 @@ function M.teardown()
|
||||
state.uv_timer:close()
|
||||
state.uv_timer = nil
|
||||
end
|
||||
cleanup_activity_file()
|
||||
if state.running and state.timer_id then
|
||||
local ok, err = api.stop_timer(state.token, state.timer_id)
|
||||
if ok then
|
||||
|
||||
Reference in New Issue
Block a user