mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 08:12:36 +08:00
Compare commits
2 Commits
feat/sidec
...
fix/instal
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
896e72d50d | ||
|
|
066e7011a1 |
26
.github/workflows/release.yml
vendored
26
.github/workflows/release.yml
vendored
@@ -45,6 +45,32 @@ jobs:
|
|||||||
node-version: '20'
|
node-version: '20'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Fetch checksums.txt from GitHub Release
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
gh release download "${GITHUB_REF_NAME}" \
|
||||||
|
--repo "${GITHUB_REPOSITORY}" \
|
||||||
|
--pattern checksums.txt \
|
||||||
|
--dir .
|
||||||
|
|
||||||
|
- name: Verify checksums.txt is present and matches current version
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -s checksums.txt
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
for plat in \
|
||||||
|
"linux-amd64.tar.gz" \
|
||||||
|
"linux-arm64.tar.gz" \
|
||||||
|
"darwin-amd64.tar.gz" \
|
||||||
|
"darwin-arm64.tar.gz" \
|
||||||
|
"windows-amd64.zip" \
|
||||||
|
"windows-arm64.zip"
|
||||||
|
do
|
||||||
|
grep -qE '^[0-9a-fA-F]{64}[[:space:]]+\*?lark-cli-'"${VERSION}"'-'"${plat}"'$' checksums.txt \
|
||||||
|
|| { echo "checksums.txt missing valid entry for lark-cli-${VERSION}-${plat}"; exit 1; }
|
||||||
|
done
|
||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
"files": [
|
"files": [
|
||||||
"scripts/install.js",
|
"scripts/install.js",
|
||||||
"scripts/run.js",
|
"scripts/run.js",
|
||||||
|
"checksums.txt",
|
||||||
"CHANGELOG.md"
|
"CHANGELOG.md"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,13 @@
|
|||||||
|
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { execSync } = require("child_process");
|
const { execFileSync } = require("child_process");
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
|
||||||
|
class ChecksumError extends Error {}
|
||||||
|
class NetworkError extends Error {}
|
||||||
|
class PackageIntegrityError extends Error {}
|
||||||
|
|
||||||
const VERSION = require("../package.json").version;
|
const VERSION = require("../package.json").version;
|
||||||
const REPO = "larksuite/cli";
|
const REPO = "larksuite/cli";
|
||||||
@@ -21,78 +26,266 @@ const ARCH_MAP = {
|
|||||||
arm64: "arm64",
|
arm64: "arm64",
|
||||||
};
|
};
|
||||||
|
|
||||||
const platform = PLATFORM_MAP[process.platform];
|
|
||||||
const arch = ARCH_MAP[process.arch];
|
|
||||||
|
|
||||||
if (!platform || !arch) {
|
|
||||||
console.error(
|
|
||||||
`Unsupported platform: ${process.platform}-${process.arch}`
|
|
||||||
);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === "win32";
|
||||||
const ext = isWindows ? ".zip" : ".tar.gz";
|
const ext = isWindows ? ".zip" : ".tar.gz";
|
||||||
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
|
||||||
const GITHUB_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
|
const ALLOWED_INITIAL_HOSTS = new Set([
|
||||||
const MIRROR_URL = `https://registry.npmmirror.com/-/binary/lark-cli/v${VERSION}/${archiveName}`;
|
"github.com",
|
||||||
|
"registry.npmmirror.com",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const CURL_CONNECT_TIMEOUT_SEC = 10;
|
||||||
|
const CURL_MAX_TIME_SEC = 120;
|
||||||
|
const CURL_MAX_REDIRS = 5;
|
||||||
|
|
||||||
|
const DEFAULT_CHECKSUM_PATH = path.join(__dirname, "..", "checksums.txt");
|
||||||
|
|
||||||
|
// Defensive: escape single quotes for PowerShell literal-string embedding.
|
||||||
|
// tmpDir comes from mkdtempSync so is controlled, but this hardens against
|
||||||
|
// future refactors that route external input into the script.
|
||||||
|
function escapeSingleQuotes(s) {
|
||||||
|
return s.replace(/'/g, "''");
|
||||||
|
}
|
||||||
|
|
||||||
const binDir = path.join(__dirname, "..", "bin");
|
const binDir = path.join(__dirname, "..", "bin");
|
||||||
const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
|
const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
|
||||||
|
|
||||||
fs.mkdirSync(binDir, { recursive: true });
|
|
||||||
|
|
||||||
function download(url, destPath) {
|
function download(url, destPath) {
|
||||||
// --ssl-revoke-best-effort: on Windows (Schannel), avoid CRYPT_E_REVOCATION_OFFLINE
|
// JS-layer pre-check: initial URL must be https and in allowlist.
|
||||||
// errors when the certificate revocation list server is unreachable
|
// Redirect targets are NOT host-checked; we rely on curl's
|
||||||
const sslFlag = isWindows ? "--ssl-revoke-best-effort " : "";
|
// --proto-redir =https + --max-redirs + SHA256 verify for safety.
|
||||||
execSync(
|
const parsed = new URL(url);
|
||||||
`curl ${sslFlag}--fail --location --silent --show-error --connect-timeout 10 --max-time 120 --output "${destPath}" "${url}"`,
|
if (parsed.protocol !== "https:") {
|
||||||
{ stdio: ["ignore", "ignore", "pipe"] }
|
throw new NetworkError(`Non-HTTPS URL rejected: ${url}`);
|
||||||
);
|
}
|
||||||
|
if (!ALLOWED_INITIAL_HOSTS.has(parsed.hostname)) {
|
||||||
|
throw new NetworkError(`Untrusted initial host: ${parsed.hostname}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"--fail", // HTTP 4xx/5xx -> non-zero exit
|
||||||
|
"--location", // follow redirects
|
||||||
|
"--proto", "=https", // initial URL: https only
|
||||||
|
"--proto-redir", "=https", // redirect targets: https only
|
||||||
|
"--max-redirs", String(CURL_MAX_REDIRS),
|
||||||
|
"--tlsv1.2", // minimum TLS 1.2
|
||||||
|
"--connect-timeout", String(CURL_CONNECT_TIMEOUT_SEC),
|
||||||
|
"--max-time", String(CURL_MAX_TIME_SEC),
|
||||||
|
"--silent", "--show-error",
|
||||||
|
"--output", destPath,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
// Schannel CRL check hard-fails when the CRL server is unreachable;
|
||||||
|
// this flag was in the original install.js and is preserved to
|
||||||
|
// avoid regression for users in corporate networks.
|
||||||
|
args.unshift("--ssl-revoke-best-effort");
|
||||||
|
}
|
||||||
|
|
||||||
|
// URL is always the last positional arg.
|
||||||
|
args.push(url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
execFileSync("curl", args, {
|
||||||
|
stdio: ["ignore", "ignore", "pipe"],
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === "ENOENT") {
|
||||||
|
// ENOENT is NOT a NetworkError: another source won't help (curl
|
||||||
|
// is missing). Throw plain Error so the fallback loop re-raises
|
||||||
|
// instead of silently trying the next URL.
|
||||||
|
throw new Error(
|
||||||
|
"curl is required for installation but was not found in PATH. " +
|
||||||
|
"Install curl or manually download the binary from " +
|
||||||
|
`https://github.com/${REPO}/releases/tag/v${VERSION}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const stderr = err.stderr ? err.stderr.toString().trim() : "";
|
||||||
|
const exitCode = err.status != null ? err.status : "unknown";
|
||||||
|
throw new NetworkError(
|
||||||
|
`curl exited with code ${exitCode}${stderr ? ": " + stderr : ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function install() {
|
function downloadWithFallback(urls, destPath) {
|
||||||
|
const attempts = [];
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
download(url, destPath);
|
||||||
|
return url;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof NetworkError) {
|
||||||
|
attempts.push({ url, error: err.message });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// ChecksumError, plain Error (ENOENT), or any other type:
|
||||||
|
// re-raise immediately without trying the next source.
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const detail = attempts
|
||||||
|
.map((a) => ` - ${a.url}\n ${a.error}`)
|
||||||
|
.join("\n");
|
||||||
|
throw new NetworkError(`All download sources failed:\n${detail}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extract(archivePath, tmpDir) {
|
||||||
|
if (isWindows) {
|
||||||
|
const script =
|
||||||
|
`$ErrorActionPreference = 'Stop'\n` +
|
||||||
|
`Expand-Archive -LiteralPath '${escapeSingleQuotes(archivePath)}' ` +
|
||||||
|
`-DestinationPath '${escapeSingleQuotes(tmpDir)}' -Force\n`;
|
||||||
|
|
||||||
|
const scriptPath = path.join(tmpDir, "extract.ps1");
|
||||||
|
fs.writeFileSync(scriptPath, script, { encoding: "utf-8" });
|
||||||
|
|
||||||
|
execFileSync("powershell", [
|
||||||
|
"-NoProfile",
|
||||||
|
"-NonInteractive",
|
||||||
|
"-ExecutionPolicy", "Bypass",
|
||||||
|
"-File", scriptPath,
|
||||||
|
], { stdio: "ignore" });
|
||||||
|
} else {
|
||||||
|
execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyChecksum(filePath, expectedHash) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hash = crypto.createHash("sha256");
|
||||||
|
const stream = fs.createReadStream(filePath);
|
||||||
|
stream.on("error", reject);
|
||||||
|
stream.on("data", (chunk) => hash.update(chunk));
|
||||||
|
stream.on("end", () => {
|
||||||
|
const actual = hash.digest("hex");
|
||||||
|
const expected = expectedHash.toLowerCase();
|
||||||
|
if (actual !== expected) {
|
||||||
|
reject(new ChecksumError(
|
||||||
|
`SHA256 mismatch for ${path.basename(filePath)}\n` +
|
||||||
|
` expected: ${expected}\n` +
|
||||||
|
` actual: ${actual}`
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getExpectedChecksum(archiveFilename, checksumPath = DEFAULT_CHECKSUM_PATH) {
|
||||||
|
if (!fs.existsSync(checksumPath)) {
|
||||||
|
// Packaging bug, not a tamper signal — routed separately.
|
||||||
|
throw new PackageIntegrityError("checksums.txt missing from package");
|
||||||
|
}
|
||||||
|
|
||||||
|
const contents = fs.readFileSync(checksumPath, "utf-8");
|
||||||
|
const lineRegex = /^([0-9a-fA-F]{64})\s+\*?(.+)$/;
|
||||||
|
|
||||||
|
for (const rawLine of contents.split("\n")) {
|
||||||
|
const line = rawLine.trim();
|
||||||
|
if (line === "" || line.startsWith("#")) continue;
|
||||||
|
|
||||||
|
const match = line.match(lineRegex);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
const [, hash, filename] = match;
|
||||||
|
if (filename.trim() === archiveFilename) {
|
||||||
|
return hash.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ChecksumError(`No checksum entry for ${archiveFilename}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function install() {
|
||||||
|
const platform = PLATFORM_MAP[process.platform];
|
||||||
|
const arch = ARCH_MAP[process.arch];
|
||||||
|
if (!platform || !arch) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported platform: ${process.platform}-${process.arch}. ` +
|
||||||
|
`Download manually from ` +
|
||||||
|
`https://github.com/${REPO}/releases/tag/v${VERSION}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
|
||||||
|
const sources = [
|
||||||
|
`https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`,
|
||||||
|
`https://registry.npmmirror.com/-/binary/lark-cli/v${VERSION}/${archiveName}`,
|
||||||
|
];
|
||||||
|
|
||||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lark-cli-"));
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lark-cli-"));
|
||||||
const archivePath = path.join(tmpDir, archiveName);
|
const archivePath = path.join(tmpDir, archiveName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
try {
|
// 1. Early fail: if the bundled checksums.txt is broken,
|
||||||
download(GITHUB_URL, archivePath);
|
// report now before spending bandwidth.
|
||||||
} catch (err) {
|
const expectedHash = getExpectedChecksum(archiveName);
|
||||||
download(MIRROR_URL, archivePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWindows) {
|
// 2. Multi-source download; only NetworkError triggers fallback.
|
||||||
execSync(
|
const sourceUrl = downloadWithFallback(sources, archivePath);
|
||||||
`powershell -Command "Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}'"`,
|
|
||||||
{ stdio: "ignore" }
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
execSync(`tar -xzf "${archivePath}" -C "${tmpDir}"`, {
|
|
||||||
stdio: "ignore",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 3. Integrity check outside the fallback loop. Mismatch aborts
|
||||||
|
// the entire install, does NOT try the next source.
|
||||||
|
await verifyChecksum(archivePath, expectedHash);
|
||||||
|
|
||||||
|
// 4. Extract (safe: bytes match the official release).
|
||||||
|
extract(archivePath, tmpDir);
|
||||||
|
|
||||||
|
// 5. Copy binary into place and chmod.
|
||||||
const binaryName = NAME + (isWindows ? ".exe" : "");
|
const binaryName = NAME + (isWindows ? ".exe" : "");
|
||||||
const extractedBinary = path.join(tmpDir, binaryName);
|
const extractedBinary = path.join(tmpDir, binaryName);
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
fs.copyFileSync(extractedBinary, dest);
|
fs.copyFileSync(extractedBinary, dest);
|
||||||
fs.chmodSync(dest, 0o755);
|
fs.chmodSync(dest, 0o755);
|
||||||
console.log(`${NAME} v${VERSION} installed successfully`);
|
|
||||||
|
console.log(
|
||||||
|
`${NAME} v${VERSION} installed successfully ` +
|
||||||
|
`(from ${new URL(sourceUrl).hostname})`
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
// 6. Always clean up the temp directory.
|
||||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (require.main === module) {
|
||||||
install();
|
install().catch((err) => {
|
||||||
} catch (err) {
|
if (err instanceof PackageIntegrityError) {
|
||||||
console.error(`Failed to install ${NAME}:`, err.message);
|
console.error(`\n${NAME} install aborted: the installed package looks broken.\n`);
|
||||||
console.error(
|
console.error(err.message);
|
||||||
`\nIf you are behind a firewall or in a restricted network, try setting a proxy:\n` +
|
console.error(
|
||||||
` export https_proxy=http://your-proxy:port\n` +
|
`\nRe-install the package; if the issue persists, please report it:\n` +
|
||||||
` npm install -g @larksuite/cli`
|
` https://github.com/${REPO}/issues\n`
|
||||||
);
|
);
|
||||||
process.exit(1);
|
} else if (err instanceof ChecksumError) {
|
||||||
|
console.error(`\n[SECURITY] ${NAME} install aborted due to integrity check failure:\n`);
|
||||||
|
console.error(err.message);
|
||||||
|
console.error(
|
||||||
|
`\nRetry the install; if it persists, report it and download manually:\n` +
|
||||||
|
` https://github.com/${REPO}/releases/tag/v${VERSION}\n`
|
||||||
|
);
|
||||||
|
} else if (err instanceof NetworkError) {
|
||||||
|
console.error(`\n${NAME} install failed due to network errors:\n`);
|
||||||
|
console.error(err.message);
|
||||||
|
console.error(
|
||||||
|
`\nIf you are behind a firewall or on a restricted network, try configuring a proxy:\n` +
|
||||||
|
` export https_proxy=http://your-proxy:port\n` +
|
||||||
|
` npm install -g @larksuite/cli\n`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.error(`\n${NAME} install failed:\n${err.stack || err.message}`);
|
||||||
|
}
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
verifyChecksum,
|
||||||
|
getExpectedChecksum,
|
||||||
|
ChecksumError,
|
||||||
|
NetworkError,
|
||||||
|
PackageIntegrityError,
|
||||||
|
};
|
||||||
|
|||||||
103
scripts/install.test.js
Normal file
103
scripts/install.test.js
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
const { test } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const fs = require("fs");
|
||||||
|
const os = require("os");
|
||||||
|
const path = require("path");
|
||||||
|
const crypto = require("crypto");
|
||||||
|
const {
|
||||||
|
verifyChecksum,
|
||||||
|
getExpectedChecksum,
|
||||||
|
ChecksumError,
|
||||||
|
PackageIntegrityError,
|
||||||
|
} = require("./install.js");
|
||||||
|
|
||||||
|
function mktmpdir() {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(), "install-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
test("verifyChecksum: correct hash resolves", async () => {
|
||||||
|
const dir = mktmpdir();
|
||||||
|
try {
|
||||||
|
const filePath = path.join(dir, "data.bin");
|
||||||
|
const bytes = Buffer.from("hello world");
|
||||||
|
fs.writeFileSync(filePath, bytes);
|
||||||
|
const correctHash = crypto.createHash("sha256").update(bytes).digest("hex");
|
||||||
|
|
||||||
|
await verifyChecksum(filePath, correctHash);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("verifyChecksum: mismatched hash throws ChecksumError", async () => {
|
||||||
|
const dir = mktmpdir();
|
||||||
|
try {
|
||||||
|
const filePath = path.join(dir, "data.bin");
|
||||||
|
fs.writeFileSync(filePath, "hello world");
|
||||||
|
const wrongHash = "0".repeat(64);
|
||||||
|
|
||||||
|
await assert.rejects(
|
||||||
|
() => verifyChecksum(filePath, wrongHash),
|
||||||
|
(err) => err instanceof ChecksumError,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getExpectedChecksum: returns hash for listed archive", () => {
|
||||||
|
const dir = mktmpdir();
|
||||||
|
try {
|
||||||
|
const checksumsPath = path.join(dir, "checksums.txt");
|
||||||
|
const knownHash = "a".repeat(64);
|
||||||
|
fs.writeFileSync(
|
||||||
|
checksumsPath,
|
||||||
|
`${knownHash} lark-cli-1.0.0-linux-amd64.tar.gz\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = getExpectedChecksum(
|
||||||
|
"lark-cli-1.0.0-linux-amd64.tar.gz",
|
||||||
|
checksumsPath,
|
||||||
|
);
|
||||||
|
assert.strictEqual(result, knownHash);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getExpectedChecksum: throws PackageIntegrityError (not ChecksumError) when checksums.txt file is absent", () => {
|
||||||
|
const dir = mktmpdir();
|
||||||
|
try {
|
||||||
|
const missingPath = path.join(dir, "does-not-exist.txt");
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => getExpectedChecksum("lark-cli-1.0.0-linux-amd64.tar.gz", missingPath),
|
||||||
|
(err) =>
|
||||||
|
err instanceof PackageIntegrityError &&
|
||||||
|
!(err instanceof ChecksumError),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getExpectedChecksum: throws ChecksumError when entry missing", () => {
|
||||||
|
const dir = mktmpdir();
|
||||||
|
try {
|
||||||
|
const checksumsPath = path.join(dir, "checksums.txt");
|
||||||
|
fs.writeFileSync(
|
||||||
|
checksumsPath,
|
||||||
|
`${"a".repeat(64)} some-other-archive.tar.gz\n`
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => getExpectedChecksum("nonexistent-archive.tar.gz", checksumsPath),
|
||||||
|
(err) => err instanceof ChecksumError,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user