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

118 lines
5.1 KiB
JavaScript
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 || "当前实例",
owner_notify: true,
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);
if (!grant.transport || grant.transport.status !== "ready") {
const transport = grant.transport || {};
output(`[LL-REPO-PUSH-TRANSPORT-BLOCKED] ${transport.diagnostic_code || "repo_push_transport_unavailable"}`);
output(`NEXT_STEP=${transport.next_step || "服务器没有返回可验证的受限推送通道;禁止重试裸 git push读取 operation receipt。"}`);
const error = new Error(transport.diagnostic_code || "repo_push_transport_unavailable");
error.receipt = grant.receipt;
throw error;
}
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 };