import "server-only";
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { DATA_ROOT } from "./paths";
/*
* Signed upload URLs for release files, so an agent can stream a file (`curl -T <file> <url>`)
* without holding an API token, and without the file passing through its context. A URL is good for
* one file name of one release, for its signer, for UPLOAD_URL_MINUTES. Uploading still needs the
* signer to have push access then, so removing a collaborator also ends their URLs.
*/
export const UPLOAD_URL_MINUTES = 30;
type Payload = { repoId: string; releaseId: string; name: string; userId: string; expiresAt: number };
const g = globalThis as unknown as { __releaseUploadKey?: Buffer };
/** Generated once and kept in the data directory, so URLs survive a restart. */
function key() {
if (g.__releaseUploadKey) return g.__releaseUploadKey;
const file = path.join(DATA_ROOT, "release-upload-key");
if (!existsSync(file)) {
mkdirSync(DATA_ROOT, { recursive: true });
try {
writeFileSync(file, randomBytes(32).toString("hex"), { mode: 0o600, flag: "wx" });
} catch (e) {
// Another request created it first.
if ((e as NodeJS.ErrnoException).code !== "EEXIST") throw e;
}
}
return (g.__releaseUploadKey = Buffer.from(readFileSync(file, "utf8").trim(), "hex"));
}
const sign = (data: string) => createHmac("sha256", key()).update(data).digest("base64url");
/** The value of an upload URL's `upload` parameter. */
export function signUpload(payload: Omit<Payload, "expiresAt">) {
const data = Buffer.from(JSON.stringify({ ...payload, expiresAt: Date.now() + UPLOAD_URL_MINUTES * 60e3 })).toString("base64url");
return `${data}.${sign(data)}`;
}
/** The upload a signed value allows, "expired", or null if it isn't one this server signed. */
export function verifyUpload(value: string): Payload | "expired" | null {
const [data, signature, extra] = value.split(".");
if (!data || !signature || extra !== undefined) return null;
const expected = Buffer.from(sign(data));
const actual = Buffer.from(signature);
if (expected.length !== actual.length || !timingSafeEqual(expected, actual)) return null;
const payload = JSON.parse(Buffer.from(data, "base64url").toString("utf8")) as Payload;
return payload.expiresAt < Date.now() ? "expired" : payload;
}