@previewio/cli 0.2.0-fd47087 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/install.mjs +42 -1
- package/package.json +1 -1
package/install.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash, createPublicKey, verify } from 'node:crypto';
|
|
1
2
|
import { chmodSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
2
3
|
import { dirname, join, resolve } from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
@@ -6,6 +7,11 @@ const PUBLIC_DOWNLOAD_ORIGIN = 'https://download.preview.io';
|
|
|
6
7
|
const VERSION_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
7
8
|
const RESERVED_VERSIONS = new Set(['latest', 'install', '.', '..']);
|
|
8
9
|
const BINARY_FETCH_TIMEOUT_MS = 120_000;
|
|
10
|
+
// Keep in sync with RELEASE_PUBLIC_KEY in ../lib/releaseManifest.ts.
|
|
11
|
+
export const RELEASE_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
12
|
+
MCowBQYDK2VwAyEARY3NBSxJGOGpQ/PSsPGwETOBdI2Nw3s8Gl6LTEuQDLE=
|
|
13
|
+
-----END PUBLIC KEY-----
|
|
14
|
+
`;
|
|
9
15
|
|
|
10
16
|
export function binaryObjectName(platform, arch) {
|
|
11
17
|
if (platform === 'darwin' && arch === 'arm64') return 'preview-darwin-arm64';
|
|
@@ -81,6 +87,28 @@ function packageVersion(packageRoot) {
|
|
|
81
87
|
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
82
88
|
}
|
|
83
89
|
|
|
90
|
+
async function fetchBytes(url, fetchImpl) {
|
|
91
|
+
const response = await fetchImpl(url, { signal: AbortSignal.timeout(BINARY_FETCH_TIMEOUT_MS) });
|
|
92
|
+
if (!response.ok) throw new Error(`Failed to download ${url} (${response.status})`);
|
|
93
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function signedDigest(origin, version, object, fetchImpl, publicKey) {
|
|
97
|
+
const [manifest, signature] = await Promise.all([
|
|
98
|
+
fetchBytes(downloadUrl(origin, version, 'manifest.json'), fetchImpl),
|
|
99
|
+
fetchBytes(downloadUrl(origin, version, 'manifest.json.sig'), fetchImpl),
|
|
100
|
+
]);
|
|
101
|
+
const decoded = Buffer.from(new TextDecoder().decode(signature).trim(), 'base64');
|
|
102
|
+
if (!verify(null, manifest, createPublicKey(publicKey), decoded)) {
|
|
103
|
+
throw new Error(`Refused Preview CLI ${version}: the release signature did not verify`);
|
|
104
|
+
}
|
|
105
|
+
const parsed = JSON.parse(new TextDecoder().decode(manifest));
|
|
106
|
+
const digest = parsed?.version === version ? parsed.files?.[object] : undefined;
|
|
107
|
+
if (typeof digest !== 'string')
|
|
108
|
+
throw new Error(`Refused Preview CLI ${version}: ${object} is not in the signed release`);
|
|
109
|
+
return digest;
|
|
110
|
+
}
|
|
111
|
+
|
|
84
112
|
async function fetchBinary(url, fetchImpl) {
|
|
85
113
|
const response = await fetchImpl(url, { signal: AbortSignal.timeout(BINARY_FETCH_TIMEOUT_MS) });
|
|
86
114
|
if (!response.ok) {
|
|
@@ -104,10 +132,23 @@ export async function install(opts = {}) {
|
|
|
104
132
|
}
|
|
105
133
|
const url = downloadUrl(opts.origin ?? PUBLIC_DOWNLOAD_ORIGIN, version, object);
|
|
106
134
|
const dest = join(opts.binDir ?? join(packageRoot, 'bin'), installedBinaryName(platform));
|
|
107
|
-
const
|
|
135
|
+
const fetchImpl = opts.fetch ?? globalThis.fetch;
|
|
136
|
+
const [bytes, digest] = await Promise.all([
|
|
137
|
+
fetchBinary(url, fetchImpl),
|
|
138
|
+
signedDigest(
|
|
139
|
+
opts.origin ?? PUBLIC_DOWNLOAD_ORIGIN,
|
|
140
|
+
version,
|
|
141
|
+
object,
|
|
142
|
+
fetchImpl,
|
|
143
|
+
opts.publicKey ?? RELEASE_PUBLIC_KEY,
|
|
144
|
+
),
|
|
145
|
+
]);
|
|
108
146
|
if (bytes.byteLength === 0 || !looksLikePublishedBinary(bytes, platform)) {
|
|
109
147
|
throw new Error(`Refused non-binary body from ${url}`);
|
|
110
148
|
}
|
|
149
|
+
if (createHash('sha256').update(bytes).digest('hex') !== digest) {
|
|
150
|
+
throw new Error(`Refused ${url}: it does not match the signed release`);
|
|
151
|
+
}
|
|
111
152
|
mkdirSync(dirname(dest), { recursive: true });
|
|
112
153
|
const tmp = `${dest}.download`;
|
|
113
154
|
writeFileSync(tmp, bytes);
|