Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/agent/skills/file-upload/scripts/upload.mjs

Raw
#!/usr/bin/env node

import { createHash, createHmac, randomBytes } from "node:crypto";
import { createReadStream, lstatSync, realpathSync } from "node:fs";
import { basename, extname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const BUCKET = "agent-artifacts";
const UPLOAD_TIMEOUT_MILLIS = 10 * 60 * 1000;

const fail = (message, status = 1) => {
  console.error(message);
  process.exit(status);
};

const sha256 = (value) => createHash("sha256").update(value).digest("hex");
const hmac = (key, value) => createHmac("sha256", key).update(value).digest();
const encodePathSegment = (value) =>
  encodeURIComponent(value).replace(/[!'()*]/g, (character) =>
    `%${character.charCodeAt(0).toString(16).toUpperCase()}`,
  );

export const createS3PutRequest = ({
  endpoint,
  accessKeyId,
  secretAccessKey,
  key,
  contentType,
  contentLength,
  payloadHash,
  now,
}) => {
  const base = new URL(endpoint);
  if (base.protocol !== "https:" || base.username || base.password || base.search || base.hash) {
    throw new Error("GATEBRIDGE_R2_ENDPOINT must be a plain HTTPS origin");
  }
  if (base.pathname !== "/") {
    throw new Error("GATEBRIDGE_R2_ENDPOINT must not contain a path");
  }
  const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
  const date = amzDate.slice(0, 8);
  const canonicalUri = `/${[BUCKET, ...key.split("/")].map(encodePathSegment).join("/")}`;
  const canonicalHeaders =
    `host:${base.host}\n` +
    `x-amz-content-sha256:${payloadHash}\n` +
    `x-amz-date:${amzDate}\n`;
  const signedHeaders = "host;x-amz-content-sha256;x-amz-date";
  const canonicalRequest = [
    "PUT",
    canonicalUri,
    "",
    canonicalHeaders,
    signedHeaders,
    payloadHash,
  ].join("\n");
  const scope = `${date}/auto/s3/aws4_request`;
  const stringToSign = ["AWS4-HMAC-SHA256", amzDate, scope, sha256(canonicalRequest)].join("\n");
  const dateKey = hmac(`AWS4${secretAccessKey}`, date);
  const regionKey = hmac(dateKey, "auto");
  const serviceKey = hmac(regionKey, "s3");
  const signingKey = hmac(serviceKey, "aws4_request");
  const signature = createHmac("sha256", signingKey).update(stringToSign).digest("hex");
  return {
    url: `${base.origin}${canonicalUri}`,
    headers: {
      Authorization:
        `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${scope}, ` +
        `SignedHeaders=${signedHeaders}, Signature=${signature}`,
      "Content-Disposition": "inline",
      "Content-Length": String(contentLength),
      "Content-Type": contentType,
      "X-Amz-Content-Sha256": payloadHash,
      "X-Amz-Date": amzDate,
    },
  };
};

const hashFile = async (file) => {
  const hash = createHash("sha256");
  for await (const chunk of createReadStream(file)) hash.update(chunk);
  return hash.digest("hex");
};

const contentTypes = {
  ".cast": "application/x-asciicast",
  ".css": "text/css;charset=utf-8",
  ".gif": "image/gif",
  ".htm": "text/html;charset=utf-8",
  ".html": "text/html;charset=utf-8",
  ".jpeg": "image/jpeg",
  ".jpg": "image/jpeg",
  ".js": "text/javascript;charset=utf-8",
  ".json": "application/json;charset=utf-8",
  ".m4v": "video/mp4",
  ".md": "text/markdown;charset=utf-8",
  ".mov": "video/quicktime",
  ".mp3": "audio/mpeg",
  ".mp4": "video/mp4",
  ".ogg": "audio/ogg",
  ".pdf": "application/pdf",
  ".png": "image/png",
  ".svg": "image/svg+xml",
  ".txt": "text/plain;charset=utf-8",
  ".wav": "audio/wav",
  ".webm": "video/webm",
  ".webp": "image/webp",
};

const main = async () => {
  const [input, retention = "1y", confirmation] = process.argv.slice(2);
  const retentions = new Set(["7d", "1y", "keep"]);
  const invalidConfirmation =
    confirmation && (retention !== "keep" || confirmation !== "--confirm-keep");
  if (!input || !retentions.has(retention) || invalidConfirmation) {
    fail("Usage: upload.mjs <file> [7d|1y|keep] [--confirm-keep]", 2);
  }
  if (retention === "keep" && confirmation !== "--confirm-keep") {
    fail("Permanent retention requires user confirmation; then pass --confirm-keep", 2);
  }
  const environmentNames = [
    "GATEBRIDGE_R2_ACCESS_KEY_ID",
    "GATEBRIDGE_R2_SECRET_ACCESS_KEY",
    "GATEBRIDGE_R2_ENDPOINT",
  ];
  for (const name of environmentNames) {
    if (!process.env[name]?.trim()) fail(`Missing ${name}. Set it before uploading.`);
  }

  const file = resolve(input);
  let stat;
  try {
    stat = lstatSync(file);
  } catch {
    fail(`File not found: ${file}`);
  }
  if (!stat.isFile()) fail(`Not a regular file: ${file}`);
  if (stat.size > 315 * 1024 * 1024) fail("Uploads are limited to 315 MiB");

  const safeName =
    basename(file).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "artifact";
  const stamp = new Date().toISOString().replace(/[-:.]/g, "");
  const key = `${retention}/${stamp}-${randomBytes(4).toString("hex")}-${safeName}`;
  const request = createS3PutRequest({
    endpoint: process.env.GATEBRIDGE_R2_ENDPOINT,
    accessKeyId: process.env.GATEBRIDGE_R2_ACCESS_KEY_ID,
    secretAccessKey: process.env.GATEBRIDGE_R2_SECRET_ACCESS_KEY,
    key,
    contentType: contentTypes[extname(file).toLowerCase()] ?? "application/octet-stream",
    contentLength: stat.size,
    payloadHash: await hashFile(file),
    now: new Date(),
  });
  const response = await fetch(request.url, {
    method: "PUT",
    headers: request.headers,
    body: createReadStream(file),
    duplex: "half",
    redirect: "error",
    signal: AbortSignal.timeout(UPLOAD_TIMEOUT_MILLIS),
  });
  if (!response.ok) {
    const details = (await response.text()).slice(0, 4096).trim();
    throw new Error(
      `R2 upload failed: ${response.status} ${response.statusText}${details ? `: ${details}` : ""}`,
    );
  }
  console.log(`https://gatebridge.link/${key}`);
};

if (
  process.argv[1]
  && realpathSync(resolve(process.argv[1])) === realpathSync(fileURLToPath(import.meta.url))
) {
  main().catch((error) => fail(error instanceof Error ? error.message : String(error)));
}