@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.
Files changed (2) hide show
  1. package/install.mjs +42 -1
  2. 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 bytes = await fetchBinary(url, opts.fetch ?? globalThis.fetch);
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@previewio/cli",
3
- "version": "0.2.0-fd47087",
3
+ "version": "0.2.1",
4
4
  "description": "Preview CLI — JSON-first client for agentic tooling",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {