117 lines
5 KiB
JavaScript
Executable file
117 lines
5 KiB
JavaScript
Executable file
#!/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") {
|
|
output(`[LL-REPO-PUSH-TRANSPORT-BLOCKED] ${grant.transport.diagnostic_code || "repo_push_transport_unavailable"}`);
|
|
output(`NEXT_STEP=${grant.transport.next_step || "读取服务器 operation receipt。"}`);
|
|
const error = new Error(grant.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 };
|