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.
- package/http.js +83 -1
- package/index.js +4 -1
- package/launcher.js +321 -30
- 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
|
-
|
|
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:
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
553
|
+
`${NPM_REGISTRY_ORIGIN}/${packageName}/${encodeURIComponent(versionOrTag)}`,
|
|
431
554
|
)
|
|
432
555
|
|
|
433
|
-
if (res.statusCode !== 200)
|
|
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
|
|
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({
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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(
|
|
885
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
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
|
}
|