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 `) * 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) { 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; }