diff --git a/.changeset/config.json b/.changeset/config.json index 165b772..d0a5686 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,11 +1,20 @@ { "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", - "changelog": ["@changesets/changelog-github", { "repo": "lightpanda-io/node-packages" }], + "changelog": [ + "@changesets/changelog-github", + { + "repo": "lightpanda-io/node-packages" + } + ], "commit": true, - "fixed": [["@lightpanda/browser"]], + "fixed": [ + [ + "@lightpanda/browser" + ] + ], "linked": [], - "access": "restricted", + "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", "ignore": [] -} +} \ No newline at end of file diff --git a/packages/browser/src/download.ts b/packages/browser/src/download.ts index 75b0281..a158fd3 100644 --- a/packages/browser/src/download.ts +++ b/packages/browser/src/download.ts @@ -16,7 +16,18 @@ import { constants, chmodSync, createWriteStream, existsSync, mkdirSync } from 'node:fs' import https from 'node:https' import { arch, exit, platform } from 'node:process' -import { DEFAULT_CACHE_FOLDER, DEFAULT_EXECUTABLE_PATH, USER_EXECUTABLE_PATH } from './utils' +import { + DEFAULT_CACHE_FOLDER, + DEFAULT_EXECUTABLE_PATH, + GITHUB_RELEASE_DATA_URL, + USER_EXECUTABLE_PATH, + checksumFile, +} from './utils' + +type GH_ASSET = { + name: string + digest: string +} const PLATFORMS = { darwin: { @@ -66,6 +77,25 @@ export const download = async (): Promise => { return new Promise((resolve, reject) => get(url, resolve, reject)) } + const getGithubHash = async (path: string) => { + try { + const f = await fetch(path) + const data = await f.json() + + const asset: GH_ASSET = data.assets.find( + (a: GH_ASSET) => a.name === `lightpanda-${platformPath}`, + ) + + if (asset) { + return asset.digest + } + + return '' + } catch (e) { + throw new Error(e) + } + } + if (platformPath) { if (USER_EXECUTABLE_PATH) { console.info('$LIGHTPANDA_EXECUTABLE_PATH found, skipping binary download…') @@ -73,11 +103,19 @@ export const download = async (): Promise => { } try { - console.info('⏳ Downloading latest version of Lightpanda browser…') - + console.info('⏳ Downloading latest version of Lightpanda browser…', '\n') await downloadBinary( `https://github.com/lightpanda-io/browser/releases/download/nightly/lightpanda-${platformPath}`, ) + + console.info('🔐 Getting and comparing checksums…', '\n') + const ghChecksum = await getGithubHash(GITHUB_RELEASE_DATA_URL) + const lpChecksum = await checksumFile(DEFAULT_EXECUTABLE_PATH) + + if (ghChecksum !== lpChecksum) { + throw new Error("🚫 Checksums don't match!") + } + chmodSync(DEFAULT_EXECUTABLE_PATH, constants.S_IRWXU) console.info('✅ Done!') diff --git a/packages/browser/src/utils.ts b/packages/browser/src/utils.ts index f2cffbd..06404ae 100644 --- a/packages/browser/src/utils.ts +++ b/packages/browser/src/utils.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import crypto, { type BinaryToTextEncoding } from 'node:crypto' +import fs from 'node:fs' import os from 'node:os' export const DEFAULT_CACHE_FOLDER = `${os.homedir()}/.cache/lightpanda-node` @@ -21,6 +24,9 @@ export const BINARY_NAME = 'lightpanda' export const USER_EXECUTABLE_PATH = process.env.LIGHTPANDA_EXECUTABLE_PATH export const DEFAULT_EXECUTABLE_PATH = `${DEFAULT_CACHE_FOLDER}/${BINARY_NAME}` +export const GITHUB_RELEASE_DATA_URL = + 'https://api.github.com/repos/lightpanda-io/browser/releases/tags/nightly' + /** * Validate a URL structure * @param {string} url URL to validate @@ -61,3 +67,23 @@ export const validatePort = (port: number): void => { export const getExecutablePath = () => { return USER_EXECUTABLE_PATH ?? DEFAULT_EXECUTABLE_PATH } + +/** + * Get checksum from file + */ +export const checksumFile = ( + filePath: string, + algorithm = 'sha256', + encoding: BinaryToTextEncoding = 'hex', +) => { + return new Promise((resolve, reject) => { + fs.readFile(filePath, (err, data) => { + if (err) { + reject(err) + } + + const hash = crypto.createHash(algorithm).update(data).digest(encoding) + resolve(`${algorithm}:${hash}`) + }) + }) +}