local M = {}
local function trim_slash(s) return (s or ""):gsub("/+$", "") end
local function url_encode(s)
return (tostring(s):gsub("([^A-Za-z0-9%-_.~])", function(ch)
return string.format("%%%02X", string.byte(ch))
end))
end
function M.client(opts)
opts = opts or {}
local engine_url = trim_slash(opts.engine_url or env.get("ASSAY_ENGINE_URL") or "")
if engine_url == "" then
error("assay.engine.core: engine_url required (or set ASSAY_ENGINE_URL)")
end
local api_key = opts.api_key or env.get("ASSAY_ADMIN_KEY")
local function build_headers(admin)
local h = { ["Content-Type"] = "application/json" }
if admin and api_key and api_key ~= "" then
h["Authorization"] = "Bearer " .. api_key
end
return h
end
local function decode(resp, allow_empty)
if resp.status >= 200 and resp.status < 300 then
if allow_empty and (resp.status == 204 or resp.body == "" or resp.body == nil) then
return nil
end
if resp.body == nil or resp.body == "" then return nil end
return json.parse(resp.body)
end
error("assay.engine.core: HTTP " .. tostring(resp.status) .. ": " .. (resp.body or ""))
end
local function get(path, admin)
return decode(http.get(engine_url .. path, { headers = build_headers(admin) }))
end
local function post(path, body, admin)
return decode(
http.post(engine_url .. path, body, { headers = build_headers(admin) }),
true
)
end
local c = {}
function c:info() return get("/api/v1/engine/core/info", false) end
function c:health() return get("/api/v1/engine/core/health", false) end
function c:active_modules() return get("/api/v1/engine/core/active-modules", false) end
c.modules = {}
function c.modules:list() return get("/api/v1/engine/core/modules", true) end
function c.modules:toggle(name, enabled)
local body = {}
if enabled ~= nil then body.enabled = enabled end
return post("/api/v1/engine/core/modules/" .. url_encode(name) .. "/toggle", body, true)
end
c.instances = {}
function c.instances:list(qopts)
qopts = qopts or {}
local parts = {}
if qopts.limit then parts[#parts + 1] = "limit=" .. tostring(qopts.limit) end
if qopts.offset then parts[#parts + 1] = "offset=" .. tostring(qopts.offset) end
local qs = (#parts > 0) and ("?" .. table.concat(parts, "&")) or ""
return get("/api/v1/engine/core/instances" .. qs, true)
end
c.audit = {}
function c.audit:list(qopts)
qopts = qopts or {}
local parts = {}
if qopts.actor then parts[#parts + 1] = "actor=" .. url_encode(qopts.actor) end
if qopts.action then parts[#parts + 1] = "action=" .. url_encode(qopts.action) end
if qopts.since then parts[#parts + 1] = "since=" .. tostring(qopts.since) end
local until_v = qopts["until"] or qopts.until_
if until_v then parts[#parts + 1] = "until=" .. tostring(until_v) end
if qopts.limit then parts[#parts + 1] = "limit=" .. tostring(qopts.limit) end
if qopts.offset then parts[#parts + 1] = "offset=" .. tostring(qopts.offset) end
local qs = (#parts > 0) and ("?" .. table.concat(parts, "&")) or ""
return get("/api/v1/engine/core/audit" .. qs, true)
end
function c:config() return get("/api/v1/engine/core/config", true) end
return c
end
return M