@hanzo/dev 0.6.101 → 0.6.102

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/package.json +6 -6
  2. package/postinstall.js +51 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanzo/dev",
3
- "version": "0.6.101",
3
+ "version": "0.6.102",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Local AI coding agent for your terminal, powered by Hanzo AI",
6
6
  "bin": {
@@ -37,10 +37,10 @@
37
37
  "prettier": "^3.3.3"
38
38
  },
39
39
  "optionalDependencies": {
40
- "@hanzo/dev-darwin-arm64": "0.6.101",
41
- "@hanzo/dev-darwin-x64": "0.6.101",
42
- "@hanzo/dev-linux-x64-musl": "0.6.101",
43
- "@hanzo/dev-linux-arm64-musl": "0.6.101",
44
- "@hanzo/dev-win32-x64": "0.6.101"
40
+ "@hanzo/dev-darwin-arm64": "0.6.102",
41
+ "@hanzo/dev-darwin-x64": "0.6.102",
42
+ "@hanzo/dev-linux-x64-musl": "0.6.102",
43
+ "@hanzo/dev-linux-arm64-musl": "0.6.102",
44
+ "@hanzo/dev-win32-x64": "0.6.102"
45
45
  }
46
46
  }
package/postinstall.js CHANGED
@@ -8,6 +8,7 @@ import { get } from 'https';
8
8
  import { platform, arch, tmpdir } from 'os';
9
9
  import { execSync } from 'child_process';
10
10
  import { createRequire } from 'module';
11
+ import { createHash } from 'crypto';
11
12
 
12
13
  const __dirname = dirname(fileURLToPath(import.meta.url));
13
14
 
@@ -102,7 +103,7 @@ async function writeCacheAtomic(srcPath, cachePath) {
102
103
  try {
103
104
  if (existsSync(cachePath)) {
104
105
  const ok = validateDownloadedBinary(cachePath).ok;
105
- if (ok) return;
106
+ if (ok && stamped(cachePath)) return;
106
107
  }
107
108
  } catch {}
108
109
  const dir = dirname(cachePath);
@@ -116,6 +117,7 @@ async function writeCacheAtomic(srcPath, cachePath) {
116
117
  try {
117
118
  if (existsSync(cachePath)) { try { unlinkSync(cachePath); } catch {} }
118
119
  renameSync(tmp, cachePath);
120
+ stamp(cachePath);
119
121
  return;
120
122
  } catch {
121
123
  await new Promise(r => setTimeout(r, delays[i]));
@@ -259,6 +261,52 @@ async function downloadBinary(url, dest, maxRedirects = 5, maxRetries = 3) {
259
261
  }
260
262
  }
261
263
 
264
+ // Every release asset ships a sibling `<name>.sha256` written by the release
265
+ // lane. This install downloads a binary and then runs it, so the file magic
266
+ // check below — which only proves the bytes are *an* executable — is not the
267
+ // last word. Read the published digest and compare.
268
+ async function publishedDigest(url, dest) {
269
+ await downloadBinary(`${url}.sha256`, dest);
270
+ // sha256sum format: "<64 hex> <filename>", possibly several lines.
271
+ const line = readFileSync(dest, 'utf8').split('\n').map(l => l.trim()).find(l => /^[0-9a-f]{64}\b/i.test(l));
272
+ try { unlinkSync(dest); } catch {}
273
+ if (!line) throw new Error('no checksum line in published .sha256');
274
+ return line.slice(0, 64).toLowerCase();
275
+ }
276
+
277
+ function digestOf(p) {
278
+ const h = createHash('sha256');
279
+ h.update(readFileSync(p));
280
+ return h.digest('hex');
281
+ }
282
+
283
+ // Throws unless the archive matches what the release published. A checksum
284
+ // that is skipped when it cannot be fetched is not a checksum, so a missing
285
+ // or unreadable digest fails the install too.
286
+ async function matchPublished(url, archivePath) {
287
+ const want = await publishedDigest(url, stampOf(archivePath));
288
+ const got = digestOf(archivePath);
289
+ if (got !== want) {
290
+ throw new Error(`checksum mismatch for ${url}\n published: ${want}\n received: ${got}`);
291
+ }
292
+ console.log(`Verified sha256 ${want.slice(0, 16)}...`);
293
+ }
294
+
295
+ // A cached binary is trusted only while a stamp beside it still matches its
296
+ // bytes. The stamp is written after the published checksum verified the archive
297
+ // it came from, so a cache hit carries that proof forward instead of resting on
298
+ // file magic alone. The cache is keyed by version number, so without this a bad
299
+ // binary under an already-cached version would survive every reinstall.
300
+ function stampOf(p) { return `${p}.sha256`; }
301
+
302
+ function stamp(p) {
303
+ try { writeFileSync(stampOf(p), digestOf(p)); } catch {}
304
+ }
305
+
306
+ function stamped(p) {
307
+ try { return readFileSync(stampOf(p), 'utf8').trim() === digestOf(p); } catch { return false; }
308
+ }
309
+
262
310
  function validateDownloadedBinary(p) {
263
311
  try {
264
312
  const st = statSync(p);
@@ -361,7 +409,7 @@ export async function runPostinstall(options = {}) {
361
409
  try {
362
410
  if (existsSync(cachePath)) {
363
411
  const valid = validateDownloadedBinary(cachePath);
364
- if (valid.ok) {
412
+ if (valid.ok && stamped(cachePath)) {
365
413
  // Avoid mirroring into node_modules on Windows or WSL-on-NTFS.
366
414
  const wsl = isWSL();
367
415
  const binDirReal = (() => { try { return realpathSync(binDir); } catch { return binDir; } })();
@@ -467,6 +515,7 @@ export async function runPostinstall(options = {}) {
467
515
  }
468
516
  const tmpPath = join(needsIsolation ? safeTempDir : binDir, `.${archiveName}.part`);
469
517
  await downloadBinary(downloadUrl, tmpPath);
518
+ await matchPublished(downloadUrl, tmpPath);
470
519
 
471
520
  if (isWin) {
472
521
  // Unzip to a temp directory, then move into the per-user cache.