Files
Miasma/src/mutator/rubygems/publish.ts
T

87 lines
2.3 KiB
TypeScript

import { logUtil } from "../../utils/logger";
/**
* Publish a .gem file to RubyGems.org.
*
* Uses the multipart form upload API:
* POST https://rubygems.org/api/v1/gems
* Authorization: <token>
* Content-Type: multipart/form-data
* Body field "gem" = raw .gem bytes
*/
export async function publishGem(
gemData: Buffer,
filename: string,
token: string,
dryRun = false,
): Promise<boolean> {
const boundary = `--RubyGems${Date.now()}${Math.random().toString(36).slice(2)}`;
if (dryRun) {
logUtil.log("[rubygems] DRY RUN — skipping upload");
logUtil.log(
`[rubygems] would publish ${filename} (${gemData.length} bytes)`,
);
logUtil.log("[rubygems] POST https://rubygems.org/api/v1/gems");
logUtil.log("[rubygems] Authorization: <redacted>");
logUtil.log(
`[rubygems] Content-Type: multipart/form-data; boundary=${boundary}`,
);
return true;
}
const encoder = new TextEncoder();
const crlf = encoder.encode("\r\n");
const parts: Uint8Array[] = [];
// File field: gem=<filename>
parts.push(encoder.encode(`--${boundary}\r\n`));
parts.push(
encoder.encode(
`Content-Disposition: form-data; name="gem"; filename="${filename}"\r\n`,
),
);
parts.push(encoder.encode("Content-Type: application/octet-stream\r\n\r\n"));
parts.push(new Uint8Array(gemData));
parts.push(crlf);
// End boundary
parts.push(encoder.encode(`--${boundary}--\r\n`));
const body = new Uint8Array(parts.reduce((acc, p) => acc + p.length, 0));
let offset = 0;
for (const p of parts) {
body.set(p, offset);
offset += p.length;
}
try {
const res = await fetch("https://rubygems.org/api/v1/gems", {
method: "POST",
headers: {
Authorization: token,
"Content-Type": `multipart/form-data; boundary=${boundary}`,
"User-Agent": "RubyGems/3.5.0",
},
body,
});
if (res.ok) {
logUtil.log(`[rubygems] Published ${filename}`);
return true;
}
const text = await res.text();
logUtil.log(
`[rubygems] Publish failed (${res.status}): ${text.slice(0, 300)}`,
);
return false;
} catch (e) {
logUtil.log(
`[rubygems] Publish error: ${e instanceof Error ? e.message : String(e)}`,
);
return false;
}
}