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

117 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 || "当前实例",
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 };