mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 00:06:28 +08:00
* refactor: make install.js side-effect-free on require Change-Id: I5444e3f34642d7c0740b6422a70ca6921a85e363 * feat: add getExpectedChecksum with unit tests Change-Id: I87548be25d30c384e743da17b1d161b9d9f0ea87 * feat: add verifyChecksum with unit tests Change-Id: Ifc2067bf1b824b02257dba7b53716fbe18d0f6b6 * feat: harden download with host allowlist and checksum verification Change-Id: I2580782866049f1f62a2597e86b7bf59d0e50925 * ci: bundle checksums.txt in npm package for install verification Change-Id: I2d7c44d9d5b9075158f63c0f8cf66c1e0abe3d8d * ci: use triggering tag and verify checksums.txt presence in release workflow Address CodeRabbit review: use GITHUB_REF_NAME instead of parsing package.json to avoid version drift, and add explicit file check to fail loudly if checksums.txt is missing or empty. Change-Id: I8a5658412b6afc338ad2a642baba146cceafd0fc * feat: streaming hash, allowlist tests, and malformed-line coverage - verifyChecksum: switch from readFileSync to streaming 64KB chunks to avoid loading entire archive (10-100MB) into memory - Export and test assertAllowedHost: 7 cases covering allowed hosts, rejection, case normalization, port handling, invalid URL - Add ALLOWED_HOSTS comment clarifying it only gates initial URL - Add getExpectedChecksum tests for malformed/tab-separated lines Change-Id: Ida639def89c242b3b261a76effae08fd414a10dc
167 lines
5.0 KiB
JavaScript
167 lines
5.0 KiB
JavaScript
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
const { describe, it } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const os = require("os");
|
|
|
|
const crypto = require("crypto");
|
|
|
|
const { getExpectedChecksum, verifyChecksum, assertAllowedHost } = require("./install.js");
|
|
|
|
describe("getExpectedChecksum", () => {
|
|
function makeTmpChecksums(content) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "checksum-test-"));
|
|
fs.writeFileSync(path.join(dir, "checksums.txt"), content, "utf8");
|
|
return dir;
|
|
}
|
|
|
|
it("returns correct hash from standard-format checksums.txt", () => {
|
|
const dir = makeTmpChecksums(
|
|
"abc123def456 lark-cli-1.0.0-darwin-arm64.tar.gz\n"
|
|
);
|
|
const hash = getExpectedChecksum(
|
|
"lark-cli-1.0.0-darwin-arm64.tar.gz",
|
|
dir
|
|
);
|
|
assert.equal(hash, "abc123def456");
|
|
});
|
|
|
|
it("returns correct entry when multiple entries exist", () => {
|
|
const dir = makeTmpChecksums(
|
|
"aaaa lark-cli-1.0.0-linux-amd64.tar.gz\n" +
|
|
"bbbb lark-cli-1.0.0-darwin-arm64.tar.gz\n" +
|
|
"cccc lark-cli-1.0.0-windows-amd64.zip\n"
|
|
);
|
|
const hash = getExpectedChecksum(
|
|
"lark-cli-1.0.0-darwin-arm64.tar.gz",
|
|
dir
|
|
);
|
|
assert.equal(hash, "bbbb");
|
|
});
|
|
|
|
it("throws Error when archiveName is not found", () => {
|
|
const dir = makeTmpChecksums(
|
|
"aaaa lark-cli-1.0.0-linux-amd64.tar.gz\n"
|
|
);
|
|
assert.throws(
|
|
() => getExpectedChecksum("nonexistent.tar.gz", dir),
|
|
{ message: /Checksum entry not found for nonexistent\.tar\.gz/ }
|
|
);
|
|
});
|
|
|
|
it("returns null when checksums.txt does not exist", () => {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "checksum-test-"));
|
|
// No checksums.txt in dir
|
|
const result = getExpectedChecksum("anything.tar.gz", dir);
|
|
assert.equal(result, null);
|
|
});
|
|
|
|
it("skips malformed lines and still finds valid entry", () => {
|
|
const dir = makeTmpChecksums(
|
|
"garbage line without separator\n" +
|
|
"\n" +
|
|
"abc123 lark-cli-1.0.0-darwin-arm64.tar.gz\n" +
|
|
"also garbage\n"
|
|
);
|
|
const hash = getExpectedChecksum(
|
|
"lark-cli-1.0.0-darwin-arm64.tar.gz",
|
|
dir
|
|
);
|
|
assert.equal(hash, "abc123");
|
|
});
|
|
|
|
it("skips tab-separated lines (only double-space is valid)", () => {
|
|
const dir = makeTmpChecksums(
|
|
"wrong\tlark-cli-1.0.0-darwin-arm64.tar.gz\n" +
|
|
"correct lark-cli-1.0.0-darwin-arm64.tar.gz\n"
|
|
);
|
|
const hash = getExpectedChecksum(
|
|
"lark-cli-1.0.0-darwin-arm64.tar.gz",
|
|
dir
|
|
);
|
|
assert.equal(hash, "correct");
|
|
});
|
|
});
|
|
|
|
describe("verifyChecksum", () => {
|
|
function makeTmpFile(content) {
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "checksum-test-"));
|
|
const filePath = path.join(dir, "archive.tar.gz");
|
|
fs.writeFileSync(filePath, content);
|
|
return filePath;
|
|
}
|
|
|
|
function sha256(content) {
|
|
return crypto.createHash("sha256").update(content).digest("hex");
|
|
}
|
|
|
|
it("returns normally when hash matches", () => {
|
|
const content = "binary content here";
|
|
const filePath = makeTmpFile(content);
|
|
const hash = sha256(content);
|
|
// Should not throw
|
|
verifyChecksum(filePath, hash);
|
|
});
|
|
|
|
it("matches case-insensitively", () => {
|
|
const content = "case test";
|
|
const filePath = makeTmpFile(content);
|
|
const hash = sha256(content).toUpperCase();
|
|
// Should not throw
|
|
verifyChecksum(filePath, hash);
|
|
});
|
|
|
|
it("throws [SECURITY]-prefixed Error on mismatch", () => {
|
|
const filePath = makeTmpFile("real content");
|
|
assert.throws(
|
|
() => verifyChecksum(filePath, "0000000000000000000000000000000000000000000000000000000000000000"),
|
|
(err) => {
|
|
assert.match(err.message, /^\[SECURITY\]/);
|
|
assert.match(err.message, /Checksum mismatch/);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("assertAllowedHost", () => {
|
|
it("accepts github.com", () => {
|
|
assertAllowedHost("https://github.com/larksuite/cli/releases/download/v1.0.0/archive.tar.gz");
|
|
});
|
|
|
|
it("accepts objects.githubusercontent.com", () => {
|
|
assertAllowedHost("https://objects.githubusercontent.com/some/path");
|
|
});
|
|
|
|
it("accepts registry.npmmirror.com", () => {
|
|
assertAllowedHost("https://registry.npmmirror.com/-/binary/lark-cli/v1.0.0/archive.tar.gz");
|
|
});
|
|
|
|
it("rejects unknown host", () => {
|
|
assert.throws(
|
|
() => assertAllowedHost("https://evil.example.com/payload"),
|
|
{ message: /Download host not allowed: evil\.example\.com/ }
|
|
);
|
|
});
|
|
|
|
it("normalizes hostname to lowercase", () => {
|
|
// URL constructor lowercases hostnames per spec
|
|
assertAllowedHost("https://GitHub.COM/larksuite/cli/releases/download/v1.0.0/a.tar.gz");
|
|
});
|
|
|
|
it("ignores port when matching hostname", () => {
|
|
// URL.hostname does not include port
|
|
assertAllowedHost("https://github.com:443/larksuite/cli/releases/download/v1.0.0/a.tar.gz");
|
|
});
|
|
|
|
it("throws on invalid URL", () => {
|
|
assert.throws(
|
|
() => assertAllowedHost("not-a-url"),
|
|
TypeError
|
|
);
|
|
});
|
|
});
|