From 58f785e53e5d7c9b028cc29c8ce21356a95358f9 Mon Sep 17 00:00:00 2001 From: Revopush Date: Thu, 15 Oct 2026 15:08:29 +0300 Subject: [PATCH] Replace an existing release signature instead of failing sign() deleted the previous .codepushrelease with rmdir, which only removes folders, so signing an already signed folder failed with ENOTDIR. Co-Authored-By: Claude Opus 5.5 --- script/sign.ts | 2 +- test/sign.ts | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 test/sign.ts diff --git a/script/sign.ts b/script/sign.ts index 166caac..cb8ca0b 100644 --- a/script/sign.ts +++ b/script/sign.ts @@ -47,7 +47,7 @@ export default async function sign(privateKeyPath: string, updateContentsPath: s if (prevSignatureExists) { console.log(`Deleting previous release signature at ${signatureFilePath}`); - await fs.rmdir(signatureFilePath); + await fs.rm(signatureFilePath, { force: true }); } const hash: string = await hashUtils.generatePackageHashFromDirectory(updateContentsPath, path.join(updateContentsPath, "..")); diff --git a/test/sign.ts b/test/sign.ts new file mode 100644 index 0000000..d009ed9 --- /dev/null +++ b/test/sign.ts @@ -0,0 +1,60 @@ +import { strict as assert } from "assert"; +import * as crypto from "crypto"; +import * as fs from "fs"; +import * as path from "path"; +import * as jwt from "jsonwebtoken"; +import * as sinon from "sinon"; +import { generatePackageHashFromDirectory } from "../script/hash-utils"; +import sign from "../script/sign"; +import { useTempDirectory } from "./helpers/temp-directory"; + +describe("sign", () => { + const dir = useTempDirectory("sign"); + + let keys: crypto.KeyPairKeyObjectResult; + let privateKeyPath: string; + let releaseFolder: string; + let signatureFile: string; + + before(() => { + keys = crypto.generateKeyPairSync("rsa", { modulusLength: 2048 }); + }); + + beforeEach(() => { + sinon.stub(console, "log"); + privateKeyPath = dir.write("private.pem", keys.privateKey.export({ type: "pkcs1", format: "pem" })); + dir.write("release/main.jsbundle", "bundle"); + releaseFolder = path.join(dir.path, "release"); + signatureFile = path.join(releaseFolder, ".codepushrelease"); + }); + + afterEach(() => sinon.restore()); + + function readClaims(): jwt.JwtPayload { + return jwt.verify(fs.readFileSync(signatureFile, "utf8"), keys.publicKey, { algorithms: ["RS256"] }) as jwt.JwtPayload; + } + + it("writes a signature with the content hash", async () => { + const expectedHash = await generatePackageHashFromDirectory(releaseFolder, dir.path); + + await sign(privateKeyPath, releaseFolder); + + const claims = readClaims(); + assert.equal(claims.claimVersion, "1.0.0"); + assert.equal(claims.contentHash, expectedHash); + }); + + it("replaces an existing signature", async () => { + await sign(privateKeyPath, releaseFolder); + const { contentHash } = readClaims(); + + await sign(privateKeyPath, releaseFolder); + + assert.equal(readClaims().contentHash, contentHash); + }); + + it("rejects a missing signing key", async () => { + await assert.rejects(sign(path.join(dir.path, "missing.pem"), releaseFolder), /was not valid/); + assert.equal(fs.existsSync(signatureFile), false); + }); +});