-- The MIT License (MIT) -- -- Copyright (C) 2026 Scott Duensing -- -- Permission is hereby granted, free of charge, to any person obtaining a copy -- of this software and associated documentation files (the "Software"), to -- deal in the Software without restriction, including without limitation the -- rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -- sell copies of the Software, and to permit persons to whom the Software is -- furnished to do so, subject to the following conditions: -- -- The above copyright notice and this permission notice shall be included in -- all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -- IN THE SOFTWARE. -- review.lua -- the operator's review of uploaded packages, run on the operator's own computer: the -- one place the repository's secret key is kept. It lists what waits, fetches a version and shows -- what it is (the package tool's own check of it), approves a version with the repository's -- signature made here, or rejects it with a note its uploader reads. The service checks the -- signature against the repository's public key; the secret key never leaves this computer. -- -- calog tools/review.lua -- review.conf [arguments] (in the server folder) -- -- It needs singemaster's lib/config.lua (singe=DIR when singemaster is not at ../../singemaster), -- and the package tool from a DVX build or the SDK. local USAGE = [[ usage: calog tools/review.lua -- [arguments] pending versions waiting for a review, oldest first fetch download one into the work folder and show what it is approve [note] sign what was fetched with the repository's key and approve it reject reject one; the note is what the uploader reads withdraw stop listing and handing out a package publish list it again review.conf: server = https://packages.example.com the service, as anyone reaches it token = ... the service's adminToken, or an administrator's session dvxpkg = /path/to/dvxpkg the package tool secret = /path/to/KPUNCH.SEC the repository's secret key work = review where fetched packages and their signatures are kept ]] local REQUIRED = { "server", "token", "dvxpkg", "secret", "work" } -- calogArgs(): the configuration, the command and its arguments, and singe=DIR if given; without -- it, singemaster is beside this repository (from this file: tools, server, the DVX tree) local plain = {} local SINGE = (calogScript():match("^(.*)[/\\]") or ".") .. "/../../../singemaster" for _, value in ipairs(calogArgs()) do local dir = value:match("^singe=(.+)$") if dir then SINGE = dir else plain[#plain + 1] = value end end if #plain < 2 then calogPrint(USAGE) calogExit(2) end local config = load(fsRead(SINGE .. "/lib/config.lua"), "@" .. SINGE .. "/lib/config.lua")() local settings, configError = config.load(plain[1], REQUIRED, {}) if not settings then calogPrint("configuration: " .. tostring(configError)) calogExit(1) end local BASE = settings.server:gsub("/+$", "") local command = plain[2] -- The service's answer: its status, the JSON it sent (or {}), and the whole reply local function call(method, path, body) local reply = httpRequest({ method = method, url = BASE .. path, body = body and jsonStringify(body) or nil, headers = { ["Content-Type"] = "application/json", Authorization = "Bearer " .. settings.token } }) local parsed = {} if reply.body and reply.body ~= "" then local ok, value = pcall(jsonParse, reply.body) if ok and type(value) == "table" then parsed = value end end return reply.status, parsed, reply end -- Stop with what the service said, when it did not say yes local function expect(status, parsed, want) if status ~= want then calogPrint(string.format("the service answered %s: %s", tostring(status), tostring(parsed.message or parsed.error or ""))) calogExit(1) end end -- Where a fetched version, and the signature made of it, are kept local function localPath(id, version, extension) return string.format("%s/%s-%s.%s", settings.work, id, version, extension) end -- The package tool, its exit status and what it printed local function dvxpkg(args) local argv = { settings.dvxpkg } for _, value in ipairs(args) do argv[#argv + 1] = value end local run = procRun(argv) return run.exit, (run.stdout or "") .. (run.stderr or "") end -- Download a version into the work folder, check its bytes against the digest the service sent, and -- show its manifest and files as the package tool reads them. Stops if the tool refuses it. local function fetch(id, version) local status, parsed, reply = call("GET", "/v1/admin/download/" .. id .. "/" .. version) expect(status, parsed, 200) if cryptoHashSha256(reply.body) ~= reply.headers["x-content-sha256"] then calogPrint("the download does not match the digest the service sent; try again") calogExit(1) end fsMkdir(settings.work) local path = localPath(id, version, "DPK") fsWrite(path, reply.body) local checked, manifest = dvxpkg({ "check", path }) if checked ~= 0 then calogPrint("the package tool refuses " .. path .. ":") calogPrint(manifest) calogExit(1) end local _, info = dvxpkg({ "info", path }) calogPrint(info) calogPrint(string.format("fetched %s (%d bytes, sha256 %s)", path, #reply.body, reply.headers["x-content-sha256"])) return path end local function pending() local status, parsed = call("GET", "/v1/admin/pending") expect(status, parsed, 200) if (parsed.count or 0) == 0 then calogPrint("nothing is waiting") return end for _, row in ipairs(parsed.pending) do calogPrint(string.format("%s %s (%s, %d bytes) from %s, %s", row.packageId, row.version, tostring(row.kind), row.size or 0, tostring(row.uploader or "the operator"), tostring(row.uploadedAt))) calogPrint(string.format(" %s%s", tostring(row.name), row.summary and (": " .. row.summary) or "")) calogPrint(string.format(" publisher's key %s", tostring(row.signerKey or "none"))) end end -- Approve what was reviewed: the fetched file (fetched now if it was not), signed here local function approve(id, version, note) local path = localPath(id, version, "DPK") if not fsExists(path) then fetch(id, version) end local signed, said = dvxpkg({ "sign", path, settings.secret, "-d" }) if signed ~= 0 then calogPrint("the package tool could not sign it:") calogPrint(said) calogExit(1) end local status, parsed = call("POST", "/v1/admin/version/state", { package = id, version = version, state = "approved", signature = fsRead(localPath(id, version, "SIG")), note = note }) expect(status, parsed, 200) calogPrint(string.format("approved %s %s", id, version)) end local function reject(id, version, note) local status, parsed = call("POST", "/v1/admin/version/state", { package = id, version = version, state = "rejected", note = note }) expect(status, parsed, 200) calogPrint(string.format("rejected %s %s", id, version)) end local function packageState(id, state) local status, parsed = call("POST", "/v1/admin/package/state", { package = id, state = state }) expect(status, parsed, 200) calogPrint(string.format("%s is %s", id, state)) end local COMMANDS = { pending = { 0, function() pending() end }, fetch = { 2, function(a) fetch(a[1], a[2]) end }, approve = { 2, function(a) approve(a[1], a[2], a[3]) end }, reject = { 3, function(a) reject(a[1], a[2], a[3]) end }, withdraw = { 1, function(a) packageState(a[1], "withdrawn") end }, publish = { 1, function(a) packageState(a[1], "published") end }, } local entry = COMMANDS[command] local arguments = { table.unpack(plain, 3) } if not entry or #arguments < entry[1] then calogPrint(USAGE) calogExit(2) end entry[2](arguments) calogExit(0)