Ctrl K
ComunidadGuías…

scripts/artifact-transfer.mjs

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// Trusted host CLI, never an MCP filesystem or credential capability.
import { createHash } from "node:crypto";
import { open, lstat, unlink } from "node:fs/promises";
import { constants } from "node:fs";
import { basename, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const MAX_BYTES = 100 * 1024 * 1024;
const digest = (bytes) => createHash("sha256").update(bytes).digest("hex");
const fail = (message) => { throw new Error(message); };
const id = (value) => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value) ? value : fail("Invalid or missing identifier.");

async function boundedBody(response, limit) {
  const chunks = []; let total = 0;
  if (!response.body) fail("Empty server response.");
  for await (const chunk of response.body) {
    total += chunk.byteLength;
    if (total > limit) fail("Response exceeds the bounded transfer limit.");
    chunks.push(chunk);
  }
  return Buffer.concat(chunks, total);
}

export async function transferArtifact(options) {
  let stage = "validation";
  try {
    const { operation, token, file, role } = options;
    if (!["upload", "download"].includes(operation)) fail("Choose upload or download.");
    if (typeof token !== "string" || !token || /[\s\u0000-\u001f]/u.test(token)) fail("Set BENCHLEDGER_TOKEN in the host environment.");
    const base = new URL(options.baseUrl);
    if (!["http:", "https:"].includes(base.protocol) || base.username || base.password || base.search || base.hash || base.pathname !== "/") fail("Base URL must be an HTTP(S) origin without credentials, query or path.");
    const projectId = id(options.projectId);
    const scope = options.projectRevisionId !== undefined
      ? { projectRevisionId: id(options.projectRevisionId) }
      : { workItemId: id(options.workItemId), workItemRevisionId: id(options.workItemRevisionId) };
    if (options.projectRevisionId !== undefined && (options.workItemId !== undefined || options.workItemRevisionId !== undefined)) fail("Choose exactly one revision scope.");
    if (!/^(source|cad|document|brief|design_record|cad_source|step|stl|three_mf|slicer_project|gcode|firmware|drawing|validation|photo|text|other)$/u.test(role ?? "")) fail("Supply an explicit valid file role.");
    if (typeof file !== "string" || !file || file.includes("\0")) fail("Supply an explicit local file path.");
    const request = async (path, method = "GET", body, binary = false) => {
      const response = await fetch(new URL(`/api/v1${path}`, base), {
        method, redirect: "error", signal: AbortSignal.timeout(60_000),
        headers: { authorization: `Bearer ${token}`, ...(body === undefined ? {} : { "content-type": binary ? "application/octet-stream" : "application/json" }) },
        ...(body === undefined ? {} : { body: binary ? body : JSON.stringify(body) }),
      });
      if (!response.ok) { await response.body?.cancel(); fail(`HTTP ${response.status}; check token scope and record state. No automatic retry.`); }
      return response;
    };
    const json = async (path, method, body, binary) => JSON.parse((await boundedBody(await request(path, method, body, binary), 1024 * 1024)).toString("utf8"));
    // HTTP persistence metadata uses revisionId; workItemId disambiguates its kind.
    const scopeMatches = (artifact) => artifact.projectId === projectId && artifact.role === role && (scope.projectRevisionId
      ? !artifact.workItemId && (artifact.projectRevisionId ?? artifact.revisionId) === scope.projectRevisionId
      : artifact.workItemId === scope.workItemId && (artifact.workItemRevisionId ?? artifact.revisionId) === scope.workItemRevisionId);
    const verify = (artifact, bytes) => {
      if (!scopeMatches(artifact)) fail("Artifact scope or role does not match the explicit request.");
      if (artifact.byteSize !== bytes.length || artifact.sha256 !== digest(bytes)) fail("Artifact SHA-256 or byte length verification failed.");
    };
    let artifact;
    if (operation === "upload") {
      stage = "local read";
      const handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
      let bytes;
      try {
        const stat = await handle.stat();
        if (!stat.isFile() || stat.size < 1 || stat.size > MAX_BYTES) fail("Upload must be a regular nonempty file no larger than 100 MiB.");
        bytes = await boundedBody({ body: handle.createReadStream({ autoClose: false }) }, MAX_BYTES);
      } finally { await handle.close(); }
      if (!bytes.length) fail("Upload must not be empty.");
      const mediaType = options.mediaType;
      if (typeof mediaType !== "string" || !mediaType || mediaType.length > 200) fail("Supply the file media type.");
      stage = "begin upload";
      const begun = await json("/artifacts/uploads", "POST", { projectId, ...scope, role, filename: basename(file), mediaType, byteSize: bytes.length, sha256: digest(bytes) });
      const uploadId = id(begun.data?.id);
      stage = "write upload";
      await json(`/artifacts/uploads/${uploadId}`, "PUT", bytes, true);
      stage = "finalize upload (an interrupted response may have committed; inspect the revision before retrying)";
      artifact = (await json(`/artifacts/uploads/${uploadId}/finalize`, "POST")).data;
      verify(artifact, bytes);
      stage = "read back finalized artifact";
      verify(await json(`/artifacts/${id(artifact.id)}`), bytes);
    } else {
      stage = "download metadata";
      const artifactId = id(options.artifactId);
      // Preflight destination, then use exclusive creation again after verification.
      try { await lstat(file); fail("Destination already exists; choose a new output path."); } catch (error) { if (error.code !== "ENOENT") throw error; }
      artifact = await json(`/artifacts/${artifactId}`);
      if (!Number.isSafeInteger(artifact.byteSize) || artifact.byteSize < 1 || artifact.byteSize > MAX_BYTES) fail("Artifact exceeds transfer limit.");
      if (!scopeMatches(artifact)) fail("Artifact scope or role does not match the explicit request.");
      stage = "download bytes";
      const bytes = await boundedBody(await request(`/artifacts/${artifactId}/download`), artifact.byteSize);
      verify(artifact, bytes);
      stage = "save verified download";
      const handle = await open(file, "wx", 0o600);
      try { await handle.writeFile(bytes); await handle.sync(); }
      catch (error) { await unlink(file); throw error; }
      finally { await handle.close(); }
    }
    return { status: operation === "upload" ? "uploaded" : "downloaded", artifactId: id(artifact.id), projectId, ...scope, role, byteSize: artifact.byteSize, sha256: artifact.sha256 };
  } catch (error) {
    // Never serialize fetch URLs, paths, bearer values, response bodies or stacks.
    const safe = /^(Choose |Set BENCHLEDGER_TOKEN|Invalid or missing|Base URL|Supply |HTTP \d|Response exceeds|Empty server|Artifact |Upload must|Destination already)/u.test(error.message ?? "") ? error.message : "Transfer failed; check connectivity, file access and server state. No automatic retry.";
    throw new Error(`${stage}: ${safe}`);
  }
}

if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
  const [operation, ...args] = process.argv.slice(2);
  if (operation === "--help") {
    console.log("Host-only artifact transfer. Set BENCHLEDGER_URL and BENCHLEDGER_TOKEN privately.\nnode scripts/artifact-transfer.mjs upload|download --file <path> --project <id> --project-revision <id> --role <role> [--media-type <type>] [--artifact <id>]\nUse --work-item <id> --work-item-revision <id> instead of --project-revision for a work-item revision. Upload needs --media-type; download needs --artifact. No overwrites or automatic retries. HTTP is only appropriate on a trusted LAN; prefer HTTPS.");
  } else {
    const keys = { "--file": "file", "--project": "projectId", "--project-revision": "projectRevisionId", "--work-item": "workItemId", "--work-item-revision": "workItemRevisionId", "--role": "role", "--media-type": "mediaType", "--artifact": "artifactId" };
    const options = { operation, baseUrl: process.env.BENCHLEDGER_URL, token: process.env.BENCHLEDGER_TOKEN };
    try {
      for (let i = 0; i < args.length; i += 2) {
        const key = keys[args[i]];
        if (!key || options[key] !== undefined || !args[i + 1] || args[i + 1].startsWith("--")) fail("Invalid arguments; use --help.");
        options[key] = args[i + 1];
      }
      console.log(JSON.stringify(await transferArtifact(options)));
    } catch (error) { console.error(error.message); process.exitCode = 1; }
  }
}