"use strict"; const crypto = require("node:crypto"); const { enqueueDeploymentEvent } = require("./deployment-event"); const DEFAULT_REPOSITORY = "bingshuo/guanghu-ice-heart"; const DEFAULT_BRANCH = "main"; const DEFAULT_TARGET = "JD-FD-PRIMARY"; const MANIFEST_PATTERN = /^deployment\/requests\/([A-Z0-9][A-Z0-9._-]{5,119})\.json$/; function safeEqualHex(expected, supplied) { if (!/^[a-f0-9]{64}$/i.test(String(supplied || ""))) return false; const left = Buffer.from(expected, "hex"); const right = Buffer.from(String(supplied), "hex"); return left.length === right.length && crypto.timingSafeEqual(left, right); } function verifyForgejoSignature(secret, rawBody, supplied) { if (String(secret || "").length < 32) return false; const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex"); return safeEqualHex(expected, String(supplied || "").replace(/^sha256=/i, "")); } function deterministicEventId(value) { const bytes = crypto.createHash("sha256").update(String(value)).digest().subarray(0, 16); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = bytes.toString("hex"); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } function parseMergedPullRequest(payload, eventName, policy = {}) { if (eventName !== "pull_request") return { state: "ignored", diagnostic_code: "not_pull_request_event" }; if (!payload || payload.action !== "closed" || payload.pull_request?.merged !== true) { return { state: "ignored", diagnostic_code: "pull_request_not_merged" }; } const pull = payload.pull_request; const repository = String(payload.repository?.full_name || pull.base?.repo?.full_name || "").toLowerCase(); const branch = String(pull.base?.ref || ""); const commit = String(pull.merge_commit_sha || "").toLowerCase(); const merger = String(pull.merged_by?.login || pull.merged_by?.username || ""); const number = Number(pull.number || pull.index || payload.number); const expectedRepository = String(policy.repository || DEFAULT_REPOSITORY).toLowerCase(); const expectedBranch = String(policy.branch || DEFAULT_BRANCH); const allowedMergers = policy.allowedMergers || []; if (repository !== expectedRepository || branch !== expectedBranch) { return { state: "rejected", diagnostic_code: "merge_target_not_allowed" }; } if (!/^[0-9a-f]{40}$/.test(commit) || !Number.isSafeInteger(number) || number < 1) { return { state: "rejected", diagnostic_code: "merge_identity_invalid" }; } if (!merger || !allowedMergers.includes(merger)) { return { state: "rejected", diagnostic_code: "human_merger_not_allowed" }; } return { state: "accepted", repository, branch, commit, merger, number }; } async function listPullRequestFiles(merge, options = {}) { const fetchImpl = options.fetchImpl || fetch; const base = String(options.forgejoApiBase || "http://127.0.0.1:3340/api/v1").replace(/\/$/, ""); const files = []; for (let page = 1; page <= 5; page += 1) { const url = `${base}/repos/${merge.repository}/pulls/${merge.number}/files?limit=100&page=${page}`; const response = await fetchImpl(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(5000), }); if (!response.ok) throw new Error(`forgejo_pull_files_unavailable:${response.status}`); const batch = await response.json(); if (!Array.isArray(batch)) throw new Error("forgejo_pull_files_invalid"); files.push(...batch); if (batch.length < 100) return files; } throw new Error("forgejo_pull_files_limit_exceeded"); } function deploymentManifests(files) { const selected = []; for (const file of files || []) { const name = String(file?.filename || ""); const match = name.match(MANIFEST_PATTERN); if (!match || file.status === "deleted") continue; selected.push({ path: name, requestId: match[1] }); } const unique = [...new Map(selected.map(item => [item.path, item])).values()]; if (unique.length > 5) throw new Error("too_many_deployment_manifests"); return unique; } async function processForgejoMergeWebhook(input, options = {}) { const rawBody = Buffer.isBuffer(input.rawBody) ? input.rawBody : Buffer.from(String(input.rawBody || "")); if (!verifyForgejoSignature(options.secret, rawBody, input.signature)) { return { ok: false, status: 401, state: "rejected", diagnostic_code: "forgejo_signature_invalid" }; } let payload; try { payload = JSON.parse(rawBody.toString("utf8")); } catch { return { ok: false, status: 400, state: "rejected", diagnostic_code: "forgejo_payload_invalid" }; } const merge = parseMergedPullRequest(payload, String(input.eventName || ""), { repository: options.repository, branch: options.branch, allowedMergers: options.allowedMergers || [], }); if (merge.state === "ignored") return { ok: true, status: 202, ...merge, queued: 0 }; if (merge.state !== "accepted") return { ok: false, status: 409, ...merge }; let manifests; try { const files = await (options.listFiles || (value => listPullRequestFiles(value, options)))(merge); manifests = deploymentManifests(files); } catch (error) { return { ok: false, status: 502, state: "rejected", diagnostic_code: String(error.message || "forgejo_pull_files_failed") }; } if (manifests.length === 0) { return { ok: true, status: 202, state: "accepted_no_deployment_manifest", queued: 0, commit_sha: merge.commit }; } const delivery = String(input.delivery || ""); if (!/^[A-Za-z0-9._:-]{8,160}$/.test(delivery)) { return { ok: false, status: 400, state: "rejected", diagnostic_code: "forgejo_delivery_invalid" }; } const deploymentRepositories = options.deploymentRepositories || {}; const queue = options.enqueue || enqueueDeploymentEvent; const events = []; for (const manifest of manifests) { const dedupeKey = `${delivery}:${merge.repository}:${merge.commit}:${manifest.requestId}`; const eventId = deterministicEventId(dedupeKey); const queued = queue({ schema: "guanghu.deployment-intent/v1", repo: merge.repository, branch: merge.branch, commit_sha: merge.commit, resource: `${manifest.requestId}@${merge.commit}`, manifest: manifest.path, workorder_id: `forgejo-pr:${merge.number}:${delivery}`, }, { repo: merge.repository, branch: merge.branch, commit_sha: merge.commit, }, options.queueDir, { authorizer_id: "ICE-GL∞", persona_id: "SYS-GLW-ZY-EXEC-0001", execution_runtime_id: "HLCC-MERGE-DEPLOY-GATE-001", target: options.target || DEFAULT_TARGET, registry: deploymentRepositories, event_id: eventId, dedupe_key: dedupeKey, }); if (!["queued_for_resident_agent", "duplicate"].includes(queued.state)) { return { ok: false, status: 409, state: "rejected", diagnostic_code: queued.diagnostic_code || "deployment_event_rejected" }; } events.push({ request_id: manifest.requestId, event_id: queued.event_id, state: queued.state }); } return { ok: true, status: 202, state: events.every(item => item.state === "duplicate") ? "duplicate" : "queued_for_resident_agent", repository: merge.repository, branch: merge.branch, commit_sha: merge.commit, pull_request: merge.number, merger: merge.merger, queued: events.filter(item => item.state === "queued_for_resident_agent").length, events, }; } module.exports = { deploymentManifests, deterministicEventId, listPullRequestFiles, parseMergedPullRequest, processForgejoMergeWebhook, verifyForgejoSignature, };