guanghu-ice-heart/server-tools/lake-lamp-authz/authorize-repo-push.js

109 lines
4.6 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
"use strict";
const DEFAULT_URL = "https://guanghulab.com/authz";
async function authorizeRepoPush(options, deps = {}) {
const fetchImpl = deps.fetch || fetch;
const sleep = deps.sleep || (milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds)));
const output = deps.output || (line => process.stdout.write(`${line}\n`));
const baseUrl = String(options.url || DEFAULT_URL).replace(/\/$/, "");
const persona = required(options.persona, "persona");
const repo = normalizeRepo(required(options.repo, "repo"));
const target = options.target || "JD-FD-PRIMARY";
const pollMilliseconds = positiveNumber(options.poll, 5000);
const request = await requestJson(fetchImpl, `${baseUrl}/api/public/workorders`, {
system_entry: "光湖语言人格系统当前实例",
origin_software: options.software || "仓库推送客户端",
origin_model: options.model || "未声明模型",
origin_instance: options.instance || "当前实例",
persona_id: persona,
persona_name: options.name || persona,
target,
scope: "repo-push",
action: "push-repository",
description: options.description || `申请推送 ${repo}`,
});
output("[LL-WORKORDER-CREATED] 无执行权申请单已创建;尚未发送邮件,也没有推送权限。");
output(`REQUEST_URL=${request.request_url}`);
output("请把 REQUEST_URL 交给冰朔并保持本命令运行。冰朔打开页面后,服务器才发送预登记邮箱邮件。");
output("不需要向冰朔索要邮箱、授权码、验证码、密码或任何令牌。");
const deadline = Date.now() + Number(request.expires_in || 900) * 1000;
let session;
while (Date.now() < deadline) {
const response = await fetchImpl(`${baseUrl}/api/workorders/${request.workorder_id}/claim`, {
method: "POST",
headers: { authorization: `Bearer ${request.claim_token}` },
});
const payload = await readPayload(response);
if (response.status === 200) { session = payload; break; }
if (response.status !== 202 || payload.error !== "approval_pending") {
throw new Error(payload.error || `claim failed (${response.status})`);
}
await sleep(pollMilliseconds);
}
if (!session) throw new Error("authorization request expired before approval");
const common = { persona_id: persona, target, scope: "repo-push" };
const map = await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/read`, common, session.session_token);
await requestJson(fetchImpl, `${baseUrl}/api/navigation-map/ack`, { ...common, map_hash: map.map_hash }, session.session_token);
const grant = await requestJson(fetchImpl, `${baseUrl}/api/repo-push/grant`, { ...common, repo }, session.session_token);
output(`[LL-REPO-PUSH-GRANTED] ${grant.repo} 已获得限时推送许可。现在重试原 git push。`);
return grant;
}
async function requestJson(fetchImpl, url, body, bearerToken = "") {
const headers = { "content-type": "application/json" };
if (bearerToken) headers.authorization = `Bearer ${bearerToken}`;
const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body) });
const payload = await readPayload(response);
if (!response.ok) throw new Error(payload.error || `request failed (${response.status})`);
return payload;
}
async function readPayload(response) {
try { return await response.json(); } catch { return {}; }
}
function normalizeRepo(value) {
const repo = String(value).trim().toLowerCase().replace(/\.git$/, "");
if (!/^bingshuo\/[a-z0-9._-]+$/.test(repo)) throw new Error("--repo must be bingshuo/<repository>");
return repo;
}
function required(value, name) {
if (!value) throw new Error(`--${name} is required`);
return String(value);
}
function positiveNumber(value, fallback) {
if (value === undefined) return fallback;
const number = Number(value);
if (!Number.isFinite(number) || number <= 0) throw new Error("--poll must be a positive millisecond value");
return number;
}
function parseArgs(argv) {
const result = {};
for (let index = 0; index < argv.length; index += 2) {
const key = String(argv[index] || "").replace(/^--/, "");
if (!key || argv[index + 1] === undefined) throw new Error(`invalid argument: ${argv[index] || ""}`);
result[key] = argv[index + 1];
}
return result;
}
async function main() {
await authorizeRepoPush(parseArgs(process.argv.slice(2)));
}
if (require.main === module) {
main().catch(error => {
process.stderr.write(`[LL-REPO-PUSH-AUTH-FAILED] ${error.message}\n`);
process.exit(1);
});
}
module.exports = { authorizeRepoPush, normalizeRepo, parseArgs };