freebuff 0.0.176 → 0.0.178

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 (4) hide show
  1. package/http.js +83 -1
  2. package/index.js +4 -1
  3. package/launcher.js +321 -30
  4. package/package.json +10 -1
package/http.js CHANGED
@@ -4,6 +4,73 @@ const fs = require('fs')
4
4
  const { pipeline } = require('stream/promises')
5
5
  const tls = require('tls')
6
6
 
7
+ /**
8
+ * Hosts a release request may be redirected to, beyond the host it started
9
+ * on. The download route on codebuff.com answers with a 302 to a GitHub
10
+ * release asset, which GitHub in turn serves from *.githubusercontent.com.
11
+ * Anything outside this list is refused: a redirect is the one place where a
12
+ * compromised or misconfigured origin could otherwise hand the download to an
13
+ * arbitrary host.
14
+ */
15
+ const REDIRECT_ALLOWED_HOSTS = new Set([
16
+ 'codebuff.com',
17
+ 'www.codebuff.com',
18
+ 'freebuff.com',
19
+ 'www.freebuff.com',
20
+ 'github.com',
21
+ ])
22
+ const REDIRECT_ALLOWED_HOST_SUFFIXES = ['.githubusercontent.com']
23
+
24
+ function isRedirectHostAllowed(hostname, originalHostname) {
25
+ const host = hostname.toLowerCase()
26
+ if (host === originalHostname.toLowerCase()) return true
27
+ if (REDIRECT_ALLOWED_HOSTS.has(host)) return true
28
+ return REDIRECT_ALLOWED_HOST_SUFFIXES.some((suffix) => host.endsWith(suffix))
29
+ }
30
+
31
+ /**
32
+ * Decide whether a redirect may be followed. Pure, so it can be tested
33
+ * without a transport.
34
+ *
35
+ * @param {string} currentUrl the URL that answered with the redirect
36
+ * @param {string} location its Location header (absolute or relative)
37
+ * @param {{ originalUrl?: string }} [options] the first URL of the chain;
38
+ * its host is always an acceptable destination
39
+ * @returns {{ ok: true, url: string } | { ok: false, reason: string }}
40
+ */
41
+ function evaluateRedirect(currentUrl, location, options = {}) {
42
+ const from = new URL(currentUrl)
43
+ let target
44
+ try {
45
+ target = new URL(location, currentUrl)
46
+ } catch {
47
+ return { ok: false, reason: `Redirect to an invalid URL: ${location}` }
48
+ }
49
+
50
+ if (!['http:', 'https:'].includes(target.protocol)) {
51
+ return {
52
+ ok: false,
53
+ reason: `Redirect to unsupported protocol: ${target.protocol}`,
54
+ }
55
+ }
56
+ if (from.protocol === 'https:' && target.protocol === 'http:') {
57
+ return {
58
+ ok: false,
59
+ reason: `Refusing redirect from https to insecure http host ${target.hostname}`,
60
+ }
61
+ }
62
+
63
+ const originalHostname = new URL(options.originalUrl || currentUrl).hostname
64
+ if (!isRedirectHostAllowed(target.hostname, originalHostname)) {
65
+ return {
66
+ ok: false,
67
+ reason: `Refusing redirect to unexpected host ${target.hostname}`,
68
+ }
69
+ }
70
+
71
+ return { ok: true, url: target.href }
72
+ }
73
+
7
74
  function createReleaseHttpClient({
8
75
  env = process.env,
9
76
  userAgent,
@@ -200,8 +267,22 @@ function createReleaseHttpClient({
200
267
  return
201
268
  }
202
269
 
203
- httpGet(new URL(res.headers.location, url).href, {
270
+ const originalUrl = options.originalUrl || url
271
+ const redirect = evaluateRedirect(url, res.headers.location, {
272
+ originalUrl,
273
+ })
274
+ if (!redirect.ok) {
275
+ const error = new Error(redirect.reason)
276
+ error.code = 'EREDIRECT_REFUSED'
277
+ error.retryable = false
278
+ error.requestUrl = url
279
+ reject(error)
280
+ return
281
+ }
282
+
283
+ httpGet(redirect.url, {
204
284
  ...options,
285
+ originalUrl,
205
286
  redirectCount: redirectCount + 1,
206
287
  })
207
288
  .then(resolve)
@@ -436,4 +517,5 @@ function createReleaseHttpClient({
436
517
 
437
518
  module.exports = {
438
519
  createReleaseHttpClient,
520
+ evaluateRedirect,
439
521
  }
package/index.js CHANGED
@@ -21,10 +21,13 @@ const { createLauncher } = require(
21
21
  : sourceLauncherPath,
22
22
  )
23
23
 
24
+ const packageJson = require('./package.json')
25
+
24
26
  const launcher = createLauncher({
25
27
  packageName: 'freebuff',
26
28
  displayName: 'Freebuff',
27
- wrapperVersion: require('./package.json').version,
29
+ wrapperVersion: packageJson.version,
30
+ binaryChecksums: packageJson.binaryChecksums ?? null,
28
31
  telemetryEvent: 'cli.update_freebuff_failed',
29
32
  })
30
33
 
package/launcher.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn } = require('child_process')
4
+ const crypto = require('crypto')
4
5
  const fs = require('fs')
5
6
  const http = require('http')
6
7
  const https = require('https')
@@ -12,11 +13,127 @@ const zlib = require('zlib')
12
13
  const tar = require('tar')
13
14
  const { createReleaseHttpClient } = require('./http')
14
15
 
16
+ const DEFAULT_DOWNLOAD_ORIGIN = 'https://codebuff.com'
17
+ const NPM_REGISTRY_ORIGIN = 'https://registry.npmjs.org'
18
+
19
+ /**
20
+ * Every binary target a release ships, in the order the release workflow
21
+ * builds them. The launcher derives its download filename from these keys and
22
+ * `write-binary-checksums.js` stamps one sha256 per key into the published
23
+ * package.json, so the two lists must agree — a test pins that.
24
+ */
25
+ const PLATFORM_TARGET_KEYS = [
26
+ 'linux-x64',
27
+ 'linux-x64-baseline',
28
+ 'linux-arm64',
29
+ 'darwin-x64',
30
+ 'darwin-arm64',
31
+ 'win32-x64',
32
+ 'win32-x64-baseline',
33
+ ]
34
+
35
+ const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '[::1]'])
36
+
37
+ /**
38
+ * Decide where release archives are downloaded from.
39
+ *
40
+ * NEXT_PUBLIC_CODEBUFF_APP_URL is read at runtime, so anyone able to set an
41
+ * environment variable could point the launcher at a plain-http origin and
42
+ * let a network attacker substitute the archive. The override is honoured
43
+ * only over https, or over http on a loopback host (local release servers in
44
+ * development and tests). Anything else is ignored with a warning and the
45
+ * default origin is used.
46
+ */
47
+ function resolveDownloadOrigin(configuredOrigin, { warn = () => {} } = {}) {
48
+ if (typeof configuredOrigin !== 'string' || configuredOrigin.trim() === '') {
49
+ return DEFAULT_DOWNLOAD_ORIGIN
50
+ }
51
+
52
+ const trimmed = configuredOrigin.trim().replace(/\/+$/, '')
53
+ let parsed
54
+ try {
55
+ parsed = new URL(trimmed)
56
+ } catch {
57
+ warn(
58
+ `Ignoring NEXT_PUBLIC_CODEBUFF_APP_URL=${configuredOrigin}: not a valid URL. Downloading from ${DEFAULT_DOWNLOAD_ORIGIN}.`,
59
+ )
60
+ return DEFAULT_DOWNLOAD_ORIGIN
61
+ }
62
+
63
+ const isLocal = LOCAL_HOSTNAMES.has(parsed.hostname.toLowerCase())
64
+ if (
65
+ parsed.protocol === 'https:' ||
66
+ (parsed.protocol === 'http:' && isLocal)
67
+ ) {
68
+ return trimmed
69
+ }
70
+
71
+ warn(
72
+ `Ignoring NEXT_PUBLIC_CODEBUFF_APP_URL=${configuredOrigin}: release downloads must use https (http is allowed only for localhost). Downloading from ${DEFAULT_DOWNLOAD_ORIGIN}.`,
73
+ )
74
+ return DEFAULT_DOWNLOAD_ORIGIN
75
+ }
76
+
77
+ function isSha256Hex(value) {
78
+ return typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value)
79
+ }
80
+
81
+ function computeFileSha256(filePath) {
82
+ return new Promise((resolve, reject) => {
83
+ const hash = crypto.createHash('sha256')
84
+ const stream = fs.createReadStream(filePath)
85
+ stream.on('error', reject)
86
+ stream.on('data', (chunk) => hash.update(chunk))
87
+ stream.on('end', () => resolve(hash.digest('hex')))
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Compare a file against an expected sha256. Fails closed: a missing or
93
+ * malformed expectation is a failure, never a pass.
94
+ *
95
+ * @returns {Promise<{ ok: boolean, actual: string | null, reason?: string }>}
96
+ */
97
+ async function verifyFileSha256(filePath, expectedSha256) {
98
+ if (!isSha256Hex(expectedSha256)) {
99
+ return {
100
+ ok: false,
101
+ actual: null,
102
+ reason: 'no sha256 checksum is published for this archive',
103
+ }
104
+ }
105
+ const actual = await computeFileSha256(filePath)
106
+ if (actual !== expectedSha256.toLowerCase()) {
107
+ return {
108
+ ok: false,
109
+ actual,
110
+ reason: `sha256 mismatch: expected ${expectedSha256.toLowerCase()}, downloaded ${actual}`,
111
+ }
112
+ }
113
+ return { ok: true, actual }
114
+ }
115
+
116
+ /**
117
+ * The checksum map published for a release, or null when the document does
118
+ * not carry one. Values are validated on use, not here.
119
+ */
120
+ function readBinaryChecksums(packageData) {
121
+ const checksums = packageData?.binaryChecksums
122
+ return checksums && typeof checksums === 'object' && !Array.isArray(checksums)
123
+ ? checksums
124
+ : null
125
+ }
126
+
15
127
  function createLauncher(productConfig) {
16
128
  const {
17
129
  packageName,
18
130
  displayName,
19
131
  wrapperVersion = null,
132
+ // The `binaryChecksums` field of the wrapper's own package.json: sha256 per
133
+ // target for the archives of the release that shares its version. Reaches
134
+ // the launcher through npm, which is a separate trust root from the
135
+ // download origin.
136
+ binaryChecksums: ownBinaryChecksums = null,
20
137
  includeTreeSitterWasm = true,
21
138
  startupBanner = [],
22
139
  telemetryEvent = 'cli.update_codebuff_failed',
@@ -228,15 +345,14 @@ function createLauncher(productConfig) {
228
345
  }
229
346
  }
230
347
 
231
- const PLATFORM_TARGETS = {
232
- 'linux-x64': `${packageName}-linux-x64.tar.gz`,
233
- 'linux-x64-baseline': `${packageName}-linux-x64-baseline.tar.gz`,
234
- 'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
235
- 'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
236
- 'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
237
- 'win32-x64': `${packageName}-win32-x64.tar.gz`,
238
- 'win32-x64-baseline': `${packageName}-win32-x64-baseline.tar.gz`,
239
- }
348
+ // target key -> release archive filename, e.g. freebuff-darwin-arm64.tar.gz.
349
+ // The same key indexes the published `binaryChecksums` map.
350
+ const PLATFORM_TARGETS = Object.fromEntries(
351
+ PLATFORM_TARGET_KEYS.map((targetKey) => [
352
+ targetKey,
353
+ `${packageName}-${targetKey}.tar.gz`,
354
+ ]),
355
+ )
240
356
 
241
357
  const BASELINE_FALLBACK_TARGETS = {
242
358
  'linux-x64': 'linux-x64-baseline',
@@ -424,23 +540,90 @@ function createLauncher(productConfig) {
424
540
  return getDefaultTargetKey()
425
541
  }
426
542
 
427
- async function getLatestVersion() {
543
+ /**
544
+ * One version document from the npm registry: `latest` or an exact version.
545
+ * Returns null when the registry cannot answer. The document is the source
546
+ * of the expected archive checksums for any release other than the
547
+ * wrapper's own, so it is fetched over TLS from the registry and never from
548
+ * the download origin.
549
+ */
550
+ async function fetchRegistryRelease(versionOrTag) {
428
551
  try {
429
552
  const res = await httpGet(
430
- `https://registry.npmjs.org/${packageName}/latest`,
553
+ `${NPM_REGISTRY_ORIGIN}/${packageName}/${encodeURIComponent(versionOrTag)}`,
431
554
  )
432
555
 
433
- if (res.statusCode !== 200) return null
556
+ if (res.statusCode !== 200) {
557
+ res.resume()
558
+ return null
559
+ }
434
560
 
435
561
  const body = await streamToString(res)
436
562
  const packageData = JSON.parse(body)
563
+ if (typeof packageData.version !== 'string' || !packageData.version) {
564
+ return null
565
+ }
437
566
 
438
- return packageData.version || null
567
+ return {
568
+ version: packageData.version,
569
+ binaryChecksums: readBinaryChecksums(packageData),
570
+ }
439
571
  } catch (error) {
440
572
  return null
441
573
  }
442
574
  }
443
575
 
576
+ async function getLatestRelease() {
577
+ return fetchRegistryRelease('latest')
578
+ }
579
+
580
+ async function getLatestVersion() {
581
+ const release = await getLatestRelease()
582
+ return release?.version ?? null
583
+ }
584
+
585
+ /**
586
+ * The checksum map to verify `version`'s archives against.
587
+ *
588
+ * Order: a map the caller already holds (the registry document that named
589
+ * the version), then the wrapper's own package.json when the versions
590
+ * match, then a registry lookup of that exact version. Null means no map
591
+ * could be found, which the caller must treat as a failure.
592
+ */
593
+ async function resolveBinaryChecksums(version, provided = null) {
594
+ if (provided && typeof provided === 'object') {
595
+ return provided
596
+ }
597
+ if (wrapperVersion && version === wrapperVersion && ownBinaryChecksums) {
598
+ return ownBinaryChecksums
599
+ }
600
+ const release = await fetchRegistryRelease(version)
601
+ return release?.binaryChecksums ?? null
602
+ }
603
+
604
+ function createChecksumError(message, version, targetKey) {
605
+ const error = new Error(message)
606
+ error.code = 'ECHECKSUM'
607
+ error.stage = 'checksum'
608
+ error.retryable = false
609
+ error.version = version
610
+ error.target = targetKey
611
+ return error
612
+ }
613
+
614
+ async function getExpectedChecksum(version, targetKey, provided = null) {
615
+ const checksums = await resolveBinaryChecksums(version, provided)
616
+ const expected = checksums?.[targetKey]
617
+ if (!isSha256Hex(expected)) {
618
+ throw createChecksumError(
619
+ `No sha256 checksum is published for ${packageName} ${version} (${targetKey}); refusing to install an unverifiable binary.`,
620
+ version,
621
+ targetKey,
622
+ )
623
+ }
624
+ return expected.toLowerCase()
625
+ }
626
+
444
627
  function streamToString(stream) {
445
628
  return new Promise((resolve, reject) => {
446
629
  let data = ''
@@ -609,6 +792,19 @@ function createLauncher(productConfig) {
609
792
  `Download source: ${formatDownloadSource(error.requestUrl)}`,
610
793
  )
611
794
  }
795
+ if (error.code === 'ECHECKSUM') {
796
+ // Not a transport failure: retrying fetches the same bytes. Either the
797
+ // package on npm carries no checksum for this target (a release bug)
798
+ // or the archive served did not match it.
799
+ console.error(
800
+ 'The archive was not installed because it could not be verified against',
801
+ )
802
+ console.error(
803
+ 'the checksum published on npm. If this persists, please report it at:',
804
+ )
805
+ console.error(' https://github.com/CodebuffAI/codebuff/issues')
806
+ return
807
+ }
612
808
  if (error.downloadedBytes > 0) {
613
809
  const total = error.totalBytes
614
810
  ? ` of ${formatBytes(error.totalBytes)}`
@@ -634,11 +830,26 @@ function createLauncher(productConfig) {
634
830
  fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
635
831
  }
636
832
 
833
+ /**
834
+ * Admit only the files a release archive is known to contain, as plain
835
+ * files at the archive root. The workflow packs exactly the binary and
836
+ * tree-sitter.wasm; anything else in an archive — a path with `..`, a
837
+ * symlink, a stray directory — is a sign of tampering, not a newer layout,
838
+ * and is dropped. preservePaths:false is tar's own guard against absolute
839
+ * and parent-traversing entries; this filter is the allowlist on top.
840
+ */
841
+ function isAllowedArchiveEntry(entryPath, entry) {
842
+ if (entry && entry.type !== 'File') return false
843
+ const normalized = String(entryPath).replace(/^(\.\/)+/, '')
844
+ if (normalized === CONFIG.binaryName) return true
845
+ return includeTreeSitterWasm && normalized === 'tree-sitter.wasm'
846
+ }
847
+
637
848
  async function downloadAndExtract(
638
849
  downloadUrl,
639
850
  version,
640
851
  targetKey,
641
- { quiet = false } = {},
852
+ { quiet = false, expectedChecksum = null } = {},
642
853
  ) {
643
854
  let attempts = 0
644
855
  const partialArchivePath = getPartialArchivePath(version, targetKey)
@@ -677,11 +888,33 @@ function createLauncher(productConfig) {
677
888
  })
678
889
  totalBytes = result.totalBytes
679
890
 
891
+ // Verify the archive as downloaded, before a single byte of it is
892
+ // unpacked. The expected hash came from npm, not from the host that
893
+ // served the archive, so the download origin (and every redirect
894
+ // hop) can no longer substitute a binary.
895
+ if (!quiet) term.write('Verifying download...')
896
+ const verification = await verifyFileSha256(
897
+ partialArchivePath,
898
+ expectedChecksum,
899
+ )
900
+ if (!verification.ok) {
901
+ removeFileIfPresent(partialArchivePath)
902
+ throw createChecksumError(
903
+ `Downloaded ${packageName} ${version} (${targetKey}) failed verification: ${verification.reason}`,
904
+ version,
905
+ targetKey,
906
+ )
907
+ }
908
+
680
909
  try {
681
910
  await pipeline(
682
911
  fs.createReadStream(partialArchivePath),
683
912
  zlib.createGunzip(),
684
- tar.x({ cwd: CONFIG.tempDownloadDir }),
913
+ tar.x({
914
+ cwd: CONFIG.tempDownloadDir,
915
+ preservePaths: false,
916
+ filter: isAllowedArchiveEntry,
917
+ }),
685
918
  )
686
919
  } catch (error) {
687
920
  // A complete archive that cannot be extracted is corrupt. Do not
@@ -726,7 +959,7 @@ function createLauncher(productConfig) {
726
959
  }
727
960
 
728
961
  trackUpdateFailed(error.message, version, {
729
- stage: 'download',
962
+ stage: error.stage || 'download',
730
963
  errorCode: error.code,
731
964
  statusCode: error.statusCode,
732
965
  target: targetKey,
@@ -759,16 +992,37 @@ function createLauncher(productConfig) {
759
992
  throw error
760
993
  }
761
994
 
762
- const downloadUrl = `${
763
- process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
764
- }/api/releases/download/${version}/${fileName}`
995
+ // Resolved before the download so an unverifiable release is refused
996
+ // without spending the bandwidth, and so a missing map surfaces as its
997
+ // own error rather than as a mismatch.
998
+ let expectedChecksum
999
+ try {
1000
+ expectedChecksum = await getExpectedChecksum(
1001
+ version,
1002
+ targetKey,
1003
+ options.binaryChecksums,
1004
+ )
1005
+ } catch (error) {
1006
+ trackUpdateFailed(error.message, version, {
1007
+ stage: 'checksum',
1008
+ errorCode: error.code,
1009
+ target: targetKey,
1010
+ })
1011
+ throw error
1012
+ }
1013
+
1014
+ const downloadOrigin = resolveDownloadOrigin(
1015
+ process.env.NEXT_PUBLIC_CODEBUFF_APP_URL,
1016
+ { warn: (message) => console.error(`⚠️ ${message}`) },
1017
+ )
1018
+ const downloadUrl = `${downloadOrigin}/api/releases/download/${version}/${fileName}`
765
1019
 
766
1020
  fs.mkdirSync(CONFIG.configDir, { recursive: true })
767
1021
  const tempBinaryPath = await downloadAndExtract(
768
1022
  downloadUrl,
769
1023
  version,
770
1024
  targetKey,
771
- options,
1025
+ { quiet: options.quiet, expectedChecksum },
772
1026
  )
773
1027
 
774
1028
  try {
@@ -881,8 +1135,12 @@ function createLauncher(productConfig) {
881
1135
  console.log(`Download complete! Starting ${displayName}...`)
882
1136
  }
883
1137
 
884
- async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
885
- const stagedBinary = await stageBinary(version, targetKey)
1138
+ async function downloadBinary(
1139
+ version,
1140
+ targetKey = getDownloadTargetKey(),
1141
+ options = {},
1142
+ ) {
1143
+ const stagedBinary = await stageBinary(version, targetKey, options)
886
1144
  installStagedBinary(stagedBinary)
887
1145
  }
888
1146
 
@@ -910,7 +1168,15 @@ function createLauncher(productConfig) {
910
1168
  // check starts, it can otherwise remain stuck forever. The wrapper and its
911
1169
  // release binary share a version, so repair that stale cache synchronously
912
1170
  // without adding a registry lookup to healthy launches.
913
- const version = requiredWrapperVersion ?? (await getLatestVersion())
1171
+ // The wrapper release carries its own checksums; a `latest` lookup hands
1172
+ // back the registry document's map alongside the version it named.
1173
+ let version = requiredWrapperVersion
1174
+ let binaryChecksums = null
1175
+ if (!version) {
1176
+ const latest = await getLatestRelease()
1177
+ version = latest?.version ?? null
1178
+ binaryChecksums = latest?.binaryChecksums ?? null
1179
+ }
914
1180
  if (!version) {
915
1181
  console.error('❌ Failed to determine latest version')
916
1182
  console.error('Please check your internet connection and try again')
@@ -918,7 +1184,9 @@ function createLauncher(productConfig) {
918
1184
  }
919
1185
 
920
1186
  try {
921
- await downloadBinary(version)
1187
+ await downloadBinary(version, getDownloadTargetKey(), {
1188
+ binaryChecksums,
1189
+ })
922
1190
  } catch (error) {
923
1191
  term.clearLine()
924
1192
  printDownloadFailure(error)
@@ -977,7 +1245,10 @@ function createLauncher(productConfig) {
977
1245
  // relaunch's download for the shared temp directory (prepareTempDownloadDir
978
1246
  // rmSyncs it) and then spend six seconds SIGKILLing a process that has
979
1247
  // already exited.
980
- if (runningProcess.exitCode !== null || runningProcess.signalCode !== null) {
1248
+ if (
1249
+ runningProcess.exitCode !== null ||
1250
+ runningProcess.signalCode !== null
1251
+ ) {
981
1252
  return
982
1253
  }
983
1254
 
@@ -986,7 +1257,8 @@ function createLauncher(productConfig) {
986
1257
  try {
987
1258
  const currentVersion = getCurrentVersion()
988
1259
 
989
- const latestVersion = await getLatestVersion()
1260
+ const latestRelease = await getLatestRelease()
1261
+ const latestVersion = latestRelease?.version ?? null
990
1262
  if (!latestVersion) return
991
1263
 
992
1264
  if (
@@ -997,7 +1269,7 @@ function createLauncher(productConfig) {
997
1269
  const stagedBinary = await stageBinary(
998
1270
  latestVersion,
999
1271
  getDownloadTargetKey(),
1000
- { quiet: true },
1272
+ { quiet: true, binaryChecksums: latestRelease.binaryChecksums },
1001
1273
  )
1002
1274
 
1003
1275
  term.clearLine()
@@ -1361,7 +1633,13 @@ function createLauncher(productConfig) {
1361
1633
  recordMachineLacksAvx2()
1362
1634
  }
1363
1635
 
1364
- const version = metadata?.version || (await getLatestVersion())
1636
+ let version = metadata?.version || null
1637
+ let binaryChecksums = null
1638
+ if (!version) {
1639
+ const latest = await getLatestRelease()
1640
+ version = latest?.version ?? null
1641
+ binaryChecksums = latest?.binaryChecksums ?? null
1642
+ }
1365
1643
  if (!version) {
1366
1644
  return false
1367
1645
  }
@@ -1377,7 +1655,7 @@ function createLauncher(productConfig) {
1377
1655
  )
1378
1656
 
1379
1657
  try {
1380
- await downloadBinary(version, fallbackTarget)
1658
+ await downloadBinary(version, fallbackTarget, { binaryChecksums })
1381
1659
  } catch (error) {
1382
1660
  term.clearLine()
1383
1661
  console.error(`Failed to download ${fallbackTarget}: ${error.message}`)
@@ -1461,9 +1739,22 @@ function createLauncher(productConfig) {
1461
1739
  getRequiredWrapperVersion,
1462
1740
  ensureBinaryReady,
1463
1741
  isTargetAllowedForThisMachine,
1742
+ resolveBinaryChecksums,
1743
+ getExpectedChecksum,
1744
+ stageBinary,
1745
+ isAllowedArchiveEntry,
1746
+ PLATFORM_TARGETS,
1464
1747
  CONFIG,
1465
1748
  },
1466
1749
  }
1467
1750
  }
1468
1751
 
1469
- module.exports = { createLauncher }
1752
+ module.exports = {
1753
+ createLauncher,
1754
+ DEFAULT_DOWNLOAD_ORIGIN,
1755
+ PLATFORM_TARGET_KEYS,
1756
+ resolveDownloadOrigin,
1757
+ computeFileSha256,
1758
+ verifyFileSha256,
1759
+ isSha256Hex,
1760
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "freebuff",
3
- "version": "0.0.176",
3
+ "version": "0.0.178",
4
4
  "description": "The world's strongest free coding agent",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -39,5 +39,14 @@
39
39
  "publishConfig": {
40
40
  "access": "public",
41
41
  "provenance": false
42
+ },
43
+ "binaryChecksums": {
44
+ "linux-x64": "d23333202a5927614a2a3dfb17c7ef6e174125155c1648a9cd597c6ffb355fba",
45
+ "linux-x64-baseline": "c800b2b25a790cd83950328f0fc56c59420dde0ba2528b1ca6a3362e1e857f53",
46
+ "linux-arm64": "da4441d6b53553c39fb50eb2419db10d505d596a5913404a7678d476f4ec3eea",
47
+ "darwin-x64": "21e3ee0bd5bd90ae544d28a402163698da4371a222e0836987e0aed7fcd88fc0",
48
+ "darwin-arm64": "237d8c29890f6a11b91b2ccaaed0b88866c4abcd3001967fc9caeb7a97e63aa3",
49
+ "win32-x64": "201d5efb84db2ae8450c4482484043d85aa79c12b429747355fafdf7b3574502",
50
+ "win32-x64-baseline": "dd4b2999d99c8d7872fe06909d36511d7ece1756c964c7d0999fdd132317d565"
42
51
  }
43
52
  }