@vibecook/truffle 0.4.7 → 0.4.9

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.
@@ -3,19 +3,34 @@
3
3
  /**
4
4
  * Postinstall script for @vibecook/truffle.
5
5
  *
6
- * Verifies the sidecar binary is available via optionalDependencies.
7
- * If missing (e.g., --no-optional), falls back to downloading from GitHub Releases.
6
+ * Primary path: the platform-specific sidecar arrives via optionalDependencies
7
+ * (npm integrity-protected). Fallback (e.g. `--no-optional`): download the
8
+ * binary from GitHub Releases over HTTPS, then verify its SHA-256 against the
9
+ * checksums shipped *inside this package* (`sidecar-checksums.json`). When a
10
+ * checksum is present we fail closed on mismatch; when absent we warn loudly
11
+ * that integrity could not be verified.
8
12
  *
9
- * This follows the esbuild pattern: optionalDependencies as primary, HTTP download as fallback.
13
+ * This replaces the previous unverified `curl | chmod +x` fallback.
10
14
  */
11
15
 
12
16
  /* eslint-disable @typescript-eslint/no-require-imports */
13
17
 
14
- const { existsSync, mkdirSync, chmodSync } = require('fs');
18
+ const {
19
+ existsSync,
20
+ mkdirSync,
21
+ chmodSync,
22
+ createWriteStream,
23
+ createReadStream,
24
+ readFileSync,
25
+ unlinkSync,
26
+ } = require('fs');
15
27
  const { join, dirname } = require('path');
16
- const { execSync } = require('child_process');
28
+ const https = require('https');
29
+ const crypto = require('crypto');
17
30
 
18
31
  const GITHUB_REPO = 'jamesyong-42/truffle';
32
+ const DOWNLOAD_TIMEOUT_MS = 60_000;
33
+ const MAX_ATTEMPTS = 3;
19
34
 
20
35
  const PLATFORM_PACKAGES = {
21
36
  'darwin-arm64': '@vibecook/truffle-sidecar-darwin-arm64',
@@ -34,18 +49,88 @@ const GITHUB_ASSETS = {
34
49
  'win32-x64': 'tsnet-sidecar-windows-amd64.exe',
35
50
  };
36
51
 
37
- function main() {
52
+ /** Download `url` to `dest` over HTTPS, following redirects, with a hard timeout. */
53
+ function httpsDownload(url, dest, timeoutMs) {
54
+ return new Promise((resolve, reject) => {
55
+ const req = https.get(url, { headers: { 'User-Agent': 'truffle-postinstall' } }, (res) => {
56
+ // GitHub release downloads redirect to a CDN — follow 3xx.
57
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
58
+ res.resume();
59
+ httpsDownload(res.headers.location, dest, timeoutMs).then(resolve, reject);
60
+ return;
61
+ }
62
+ if (res.statusCode !== 200) {
63
+ res.resume();
64
+ reject(new Error(`HTTP ${res.statusCode}`));
65
+ return;
66
+ }
67
+ const file = createWriteStream(dest);
68
+ res.pipe(file);
69
+ file.on('finish', () => file.close((err) => (err ? reject(err) : resolve())));
70
+ file.on('error', (err) => {
71
+ try {
72
+ unlinkSync(dest);
73
+ } catch {
74
+ /* ignore */
75
+ }
76
+ reject(err);
77
+ });
78
+ });
79
+ req.setTimeout(timeoutMs, () => req.destroy(new Error(`timed out after ${timeoutMs}ms`)));
80
+ req.on('error', reject);
81
+ });
82
+ }
83
+
84
+ /** Compute the hex SHA-256 of a file. */
85
+ function sha256File(path) {
86
+ return new Promise((resolve, reject) => {
87
+ const hash = crypto.createHash('sha256');
88
+ const s = createReadStream(path);
89
+ s.on('data', (d) => hash.update(d));
90
+ s.on('end', () => resolve(hash.digest('hex')));
91
+ s.on('error', reject);
92
+ });
93
+ }
94
+
95
+ /** Look up the expected SHA-256 for a given version/asset from the shipped checksum map. */
96
+ function loadExpectedChecksum(version, asset) {
97
+ try {
98
+ const map = JSON.parse(readFileSync(join(__dirname, '..', 'sidecar-checksums.json'), 'utf8'));
99
+ const byVersion = map[version] || map[`v${version}`];
100
+ return byVersion ? byVersion[asset] : undefined;
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ /** Release tags are `truffle-v{version}` (release-please monorepo tag scheme), NOT `v{version}`. */
107
+ function buildDownloadUrl(version, asset) {
108
+ return `https://github.com/${GITHUB_REPO}/releases/download/truffle-v${version}/${asset}`;
109
+ }
110
+
111
+ /** Throw if `actual` does not match `expected` (hex, case-insensitive). */
112
+ function assertChecksum(actual, expected, label) {
113
+ if (actual.toLowerCase() !== expected.toLowerCase()) {
114
+ throw new Error(
115
+ `SECURITY: sidecar checksum mismatch for ${label}. Expected ${expected}, got ${actual}.`,
116
+ );
117
+ }
118
+ }
119
+
120
+ async function main() {
38
121
  const key = `${process.platform}-${process.arch}`;
39
122
  const pkg = PLATFORM_PACKAGES[key];
40
123
  const ext = process.platform === 'win32' ? '.exe' : '';
41
124
  const binName = `sidecar-slim${ext}`;
42
125
 
43
126
  if (!pkg) {
44
- console.warn(`[truffle] No prebuilt sidecar for ${key}. Build from source: cd packages/sidecar-slim && go build`);
127
+ console.warn(
128
+ `[truffle] No prebuilt sidecar for ${key}. Build from source: cd packages/sidecar-slim && go build`,
129
+ );
45
130
  return;
46
131
  }
47
132
 
48
- // Check if the optionalDependency was installed
133
+ // Primary: was the platform optionalDependency installed?
49
134
  try {
50
135
  const pkgJson = require.resolve(`${pkg}/package.json`);
51
136
  const binPath = join(dirname(pkgJson), 'bin', binName);
@@ -66,32 +151,76 @@ function main() {
66
151
  // Not installed — fall through to download
67
152
  }
68
153
 
69
- // Fallback: download from GitHub Releases
154
+ // Fallback: download from GitHub Releases (verified).
70
155
  const asset = GITHUB_ASSETS[key];
71
156
  if (!asset) return;
72
157
 
73
158
  const version = require('../package.json').version;
74
- const url = `https://github.com/${GITHUB_REPO}/releases/download/v${version}/${asset}`;
159
+ const url = buildDownloadUrl(version, asset);
160
+ const expected = loadExpectedChecksum(version, asset);
75
161
 
76
162
  console.log(`[truffle] Sidecar not found via npm. Downloading from GitHub Releases...`);
77
163
 
78
- try {
79
- const binDir = join(__dirname, '..', 'bin');
80
- mkdirSync(binDir, { recursive: true });
81
- const dest = join(binDir, binName);
82
-
83
- execSync(`curl -fsSL "${url}" -o "${dest}"`, { stdio: 'pipe' });
84
-
85
- if (process.platform !== 'win32') {
86
- chmodSync(dest, 0o755);
164
+ const binDir = join(__dirname, '..', 'bin');
165
+ const dest = join(binDir, binName);
166
+
167
+ // Retry only the network download; a checksum mismatch is deterministic and
168
+ // must NOT be retried (it would just re-download the same bad bytes).
169
+ let downloaded = false;
170
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
171
+ try {
172
+ mkdirSync(binDir, { recursive: true });
173
+ await httpsDownload(url, dest, DOWNLOAD_TIMEOUT_MS);
174
+ downloaded = true;
175
+ break;
176
+ } catch (err) {
177
+ console.warn(`[truffle] Download attempt ${attempt}/${MAX_ATTEMPTS} failed: ${err.message}`);
87
178
  }
179
+ }
88
180
 
89
- console.log(`[truffle] Sidecar downloaded successfully.`);
90
- } catch {
181
+ if (!downloaded) {
91
182
  console.warn(`[truffle] Could not download sidecar binary.`);
92
183
  console.warn(`[truffle] Download manually: ${url}`);
93
- console.warn(`[truffle] Or build from source: cd packages/sidecar-slim && go build -o bin/sidecar-slim`);
184
+ console.warn(
185
+ `[truffle] Or build from source: cd packages/sidecar-slim && go build -o bin/sidecar-slim`,
186
+ );
187
+ return;
94
188
  }
189
+
190
+ // Integrity check.
191
+ if (expected) {
192
+ const actual = await sha256File(dest);
193
+ try {
194
+ assertChecksum(actual, expected, `truffle-v${version}/${asset}`);
195
+ } catch (err) {
196
+ try {
197
+ unlinkSync(dest);
198
+ } catch {
199
+ /* ignore */
200
+ }
201
+ console.error(`[truffle] ${err.message} Deleted the download and refusing to install it.`);
202
+ process.exitCode = 1;
203
+ return;
204
+ }
205
+ } else {
206
+ console.warn(
207
+ `[truffle] WARNING: no pinned checksum for truffle-v${version}/${asset}; integrity NOT verified. ` +
208
+ `Prefer installing with optionalDependencies enabled.`,
209
+ );
210
+ }
211
+
212
+ if (process.platform !== 'win32') {
213
+ chmodSync(dest, 0o755);
214
+ }
215
+
216
+ console.log(`[truffle] Sidecar downloaded${expected ? ' and verified' : ''}.`);
95
217
  }
96
218
 
97
- main();
219
+ module.exports = { buildDownloadUrl, assertChecksum, loadExpectedChecksum };
220
+
221
+ if (require.main === module) {
222
+ main().catch((err) => {
223
+ console.error(`[truffle] postinstall failed: ${err.message}`);
224
+ process.exitCode = 1;
225
+ });
226
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "_comment": "SHA-256 of each GitHub Release sidecar asset, keyed by release version then by asset filename. This file ships inside the npm package (which npm integrity-protects), so it is the trust anchor for the postinstall HTTPS download fallback. When an entry for the installing version/asset is present, postinstall FAILS CLOSED on a checksum mismatch; when absent it warns that integrity was not verified. Populate this at release time, e.g. `shasum -a 256 tsnet-sidecar-*` over the built assets. Keys must match GITHUB_ASSETS in scripts/postinstall.cjs.",
3
+ "0.4.8": {
4
+ "tsnet-sidecar-darwin-arm64": "acfaed6a2de2cb250adb5d3ce5122f5530005a331ad8eb539af6376fe79fc69a",
5
+ "tsnet-sidecar-darwin-amd64": "68bcbf7bb6e61de57e357f0940ef4db8dd86ab2bd69d34e48264ff5d945a6e6c",
6
+ "tsnet-sidecar-linux-amd64": "a4450bbbf72777b9773207b96a9585040cbf27ea98f0c02a6e186a288b5d85c2",
7
+ "tsnet-sidecar-linux-arm64": "90482a6f6b671873d58c57c466c6bd0390573b6b150a98ec4648d1634a1000b2",
8
+ "tsnet-sidecar-windows-amd64.exe": "62db3a5d3fea8b917851000107cd3ede08dbc0245429693c6baf2f2e0b87c3e7"
9
+ }
10
+ }