@lpm-registry/cli 0.57.0 → 0.59.0
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/README.md +21 -45
- package/bin/lpm.js +2 -354
- package/bin/lpx.js +4 -0
- package/bin/native.js +154 -0
- package/package.json +40 -82
- package/CHANGELOG.md +0 -197
- package/LICENSE +0 -15
- package/index.d.ts +0 -131
- package/index.js +0 -31
- package/lib/api.js +0 -398
- package/lib/commands/add.js +0 -1217
- package/lib/commands/audit.js +0 -283
- package/lib/commands/cache.js +0 -209
- package/lib/commands/check-name.js +0 -112
- package/lib/commands/config.js +0 -186
- package/lib/commands/doctor.js +0 -140
- package/lib/commands/info.js +0 -215
- package/lib/commands/init.js +0 -146
- package/lib/commands/install.js +0 -264
- package/lib/commands/login.js +0 -595
- package/lib/commands/logout.js +0 -94
- package/lib/commands/marketplace-compare.js +0 -164
- package/lib/commands/marketplace-earnings.js +0 -89
- package/lib/commands/mcp-setup.js +0 -363
- package/lib/commands/npmrc.js +0 -178
- package/lib/commands/open.js +0 -82
- package/lib/commands/outdated.js +0 -291
- package/lib/commands/pool-stats.js +0 -100
- package/lib/commands/publish.js +0 -949
- package/lib/commands/quality.js +0 -211
- package/lib/commands/remove.js +0 -82
- package/lib/commands/run.js +0 -14
- package/lib/commands/search.js +0 -143
- package/lib/commands/setup.js +0 -221
- package/lib/commands/skills.js +0 -898
- package/lib/commands/token-rotate.js +0 -25
- package/lib/commands/uninstall.js +0 -107
- package/lib/commands/whoami.js +0 -129
- package/lib/config.js +0 -328
- package/lib/constants.js +0 -190
- package/lib/ecosystem.js +0 -501
- package/lib/editor-skills.js +0 -321
- package/lib/editors.js +0 -215
- package/lib/import-rewriter.js +0 -364
- package/lib/install-targets/mcp-server.js +0 -245
- package/lib/install-targets/vscode-extension.js +0 -178
- package/lib/install-targets.js +0 -82
- package/lib/integrity.js +0 -179
- package/lib/lpm-config-prompts.js +0 -102
- package/lib/lpm-config.js +0 -408
- package/lib/oidc.js +0 -166
- package/lib/project-utils.js +0 -152
- package/lib/quality/checks.js +0 -687
- package/lib/quality/display.js +0 -155
- package/lib/quality/score.js +0 -118
- package/lib/quality/swift-checks.js +0 -447
- package/lib/safe-path.js +0 -180
- package/lib/secure-store.js +0 -321
- package/lib/swift-project.js +0 -637
- package/lib/ui.js +0 -40
- package/scripts/__tests__/install-binary.test.js +0 -346
- package/scripts/install-binary.js +0 -571
|
@@ -1,571 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* LPM CLI Binary Installer — 3-tier fallback + hard-link optimization.
|
|
5
|
-
*
|
|
6
|
-
* Runs as a postinstall hook. Resolution order:
|
|
7
|
-
*
|
|
8
|
-
* 1. optionalDependencies — platform package installed by npm (fastest, cached).
|
|
9
|
-
* Integrity story: npm provenance attestation, verified by the npm registry
|
|
10
|
-
* at install time. No extra check needed here.
|
|
11
|
-
* 2. GitHub Releases — direct download if optionalDependencies missing
|
|
12
|
-
* (--no-optional, corporate proxies blocking scoped packages, resolution
|
|
13
|
-
* failure). Integrity story: every byte is anchored to a Sigstore-signed
|
|
14
|
-
* `SHA256SUMS.txt` manifest published per release. The `sigstore` npm
|
|
15
|
-
* package is consulted as an optional dependency — when resolvable, full
|
|
16
|
-
* Sigstore bundle verification runs; when not, SHA-256 against the signed
|
|
17
|
-
* manifest is the floor. Fail-closed on any integrity gate.
|
|
18
|
-
* 3. Graceful fallback — JS CLI handles commands if Tier 2 returns null.
|
|
19
|
-
*
|
|
20
|
-
* After resolving the binary, hard-links it over bin/lpm.js so subsequent
|
|
21
|
-
* runs execute the native binary directly (zero Node.js overhead).
|
|
22
|
-
*
|
|
23
|
-
* Set `LPM_INSTALL_INSECURE=1` to skip every Tier-2 integrity check (escape
|
|
24
|
-
* valve for emergency installs from releases that predate the signed-install
|
|
25
|
-
* gate). Emits a loud WARN on every use.
|
|
26
|
-
*
|
|
27
|
-
* Follows the esbuild postinstall pattern: https://github.com/evanw/esbuild
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
import { execSync } from "node:child_process"
|
|
31
|
-
import crypto from "node:crypto"
|
|
32
|
-
import fs from "node:fs"
|
|
33
|
-
import https from "node:https"
|
|
34
|
-
import { createRequire } from "node:module"
|
|
35
|
-
import os from "node:os"
|
|
36
|
-
import path from "node:path"
|
|
37
|
-
import { fileURLToPath, pathToFileURL } from "node:url"
|
|
38
|
-
|
|
39
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
40
|
-
const pkgDir = path.join(__dirname, "..")
|
|
41
|
-
const binDir = path.join(pkgDir, "bin")
|
|
42
|
-
const require = createRequire(import.meta.url)
|
|
43
|
-
|
|
44
|
-
const PLATFORMS = {
|
|
45
|
-
"darwin-arm64": { pkg: "@lpm-registry/cli-darwin-arm64", binary: "lpm" },
|
|
46
|
-
"darwin-x64": { pkg: "@lpm-registry/cli-darwin-x64", binary: "lpm" },
|
|
47
|
-
"linux-x64": { pkg: "@lpm-registry/cli-linux-x64", binary: "lpm" },
|
|
48
|
-
"linux-arm64": { pkg: "@lpm-registry/cli-linux-arm64", binary: "lpm" },
|
|
49
|
-
"win32-x64": { pkg: "@lpm-registry/cli-win32-x64", binary: "lpm.exe" },
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const GITHUB_RELEASES =
|
|
53
|
-
"https://github.com/lpm-dev/rust-client/releases/download"
|
|
54
|
-
const GITHUB_BINARY_NAMES = {
|
|
55
|
-
"darwin-arm64": "lpm-darwin-arm64",
|
|
56
|
-
"darwin-x64": "lpm-darwin-x64",
|
|
57
|
-
"linux-x64": "lpm-linux-x64",
|
|
58
|
-
"linux-arm64": "lpm-linux-arm64",
|
|
59
|
-
"win32-x64": "lpm-win32-x64.exe",
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const MANIFEST_MAX_BYTES = 64 * 1024
|
|
63
|
-
const BUNDLE_MAX_BYTES = 1024 * 1024
|
|
64
|
-
const ASSET_MAX_BYTES = 256 * 1024 * 1024
|
|
65
|
-
|
|
66
|
-
const SIGSTORE_IDENTITY = {
|
|
67
|
-
certificateIdentityRegExp:
|
|
68
|
-
"^https://github\\.com/lpm-dev/rust-client/\\.github/workflows/release\\.yml@.+",
|
|
69
|
-
certificateOIDCIssuer: "https://token.actions.githubusercontent.com",
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export async function main() {
|
|
73
|
-
const platform = `${process.platform}-${os.arch()}`
|
|
74
|
-
const platformInfo = PLATFORMS[platform]
|
|
75
|
-
|
|
76
|
-
if (!platformInfo) {
|
|
77
|
-
console.log(
|
|
78
|
-
`[lpm] No pre-built binary for ${platform}. Using JS CLI fallback.`,
|
|
79
|
-
)
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const pkg = JSON.parse(
|
|
84
|
-
fs.readFileSync(path.join(pkgDir, "package.json"), "utf-8"),
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
let binaryPath = tryOptionalDependency(platformInfo)
|
|
88
|
-
|
|
89
|
-
if (!binaryPath) {
|
|
90
|
-
const githubBinaryName = GITHUB_BINARY_NAMES[platform]
|
|
91
|
-
if (githubBinaryName) {
|
|
92
|
-
binaryPath = await tryGitHubDownload(pkg.version, githubBinaryName)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (!binaryPath) {
|
|
97
|
-
console.log("[lpm] Binary not available. Using JS CLI fallback.")
|
|
98
|
-
return
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (process.platform !== "win32") {
|
|
102
|
-
tryHardLink(binaryPath)
|
|
103
|
-
} else {
|
|
104
|
-
tryCopyBinary(binaryPath)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function tryOptionalDependency(platformInfo) {
|
|
109
|
-
try {
|
|
110
|
-
const pkgJsonPath = require.resolve(`${platformInfo.pkg}/package.json`)
|
|
111
|
-
const platformPkgDir = path.dirname(pkgJsonPath)
|
|
112
|
-
const binaryPath = path.join(platformPkgDir, platformInfo.binary)
|
|
113
|
-
|
|
114
|
-
if (fs.existsSync(binaryPath)) {
|
|
115
|
-
try {
|
|
116
|
-
execSync(`"${binaryPath}" --version`, {
|
|
117
|
-
stdio: "ignore",
|
|
118
|
-
timeout: 5000,
|
|
119
|
-
})
|
|
120
|
-
console.log(`[lpm] Using native binary from ${platformInfo.pkg}`)
|
|
121
|
-
return binaryPath
|
|
122
|
-
} catch {
|
|
123
|
-
console.log(
|
|
124
|
-
`[lpm] Binary from ${platformInfo.pkg} exists but not executable.`,
|
|
125
|
-
)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
} catch {
|
|
129
|
-
// Package not installed (--no-optional, or resolution failed).
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Tier 2: download binary from GitHub Releases with end-to-end integrity.
|
|
137
|
-
*
|
|
138
|
-
* Returns the path to the verified, executable binary on success, or null
|
|
139
|
-
* on any failure (manifest 404, SHA mismatch, Sigstore rejection, network
|
|
140
|
-
* error). `null` falls through to the JS CLI fallback in `main` — Tier 2
|
|
141
|
-
* never silently installs an unverified binary.
|
|
142
|
-
*
|
|
143
|
-
* Exported for testability — `vi.mock` of the network calls drives this
|
|
144
|
-
* function directly.
|
|
145
|
-
*/
|
|
146
|
-
export async function tryGitHubDownload(version, githubBinaryName, deps = {}) {
|
|
147
|
-
const downloadFn = deps.download || download
|
|
148
|
-
const downloadToBufferFn = deps.downloadToBuffer || downloadToBuffer
|
|
149
|
-
const sha256FileFn = deps.sha256File || sha256File
|
|
150
|
-
const verifyFn = deps.tryVerifyManifestSignature || tryVerifyManifestSignature
|
|
151
|
-
const destDir = deps.destDir || binDir
|
|
152
|
-
const insecure = process.env.LPM_INSTALL_INSECURE === "1"
|
|
153
|
-
|
|
154
|
-
const manifestUrl = `${GITHUB_RELEASES}/v${version}/SHA256SUMS.txt`
|
|
155
|
-
const bundleUrl = `${GITHUB_RELEASES}/v${version}/SHA256SUMS.txt.sigstore`
|
|
156
|
-
const assetUrl = `${GITHUB_RELEASES}/v${version}/${githubBinaryName}`
|
|
157
|
-
|
|
158
|
-
let manifestBody = null
|
|
159
|
-
let bundleBody = null
|
|
160
|
-
|
|
161
|
-
if (insecure) {
|
|
162
|
-
console.log(
|
|
163
|
-
"[lpm] WARN: LPM_INSTALL_INSECURE=1 — skipping ALL integrity verification on Tier-2 download",
|
|
164
|
-
)
|
|
165
|
-
} else {
|
|
166
|
-
try {
|
|
167
|
-
manifestBody = await downloadToBufferFn(manifestUrl, MANIFEST_MAX_BYTES)
|
|
168
|
-
} catch (err) {
|
|
169
|
-
if (err && err.statusCode === 404) {
|
|
170
|
-
console.log(
|
|
171
|
-
`[lpm] ERROR: release v${version} does not ship SHA256SUMS.txt — this release predates LPM's signed-install gate.`,
|
|
172
|
-
)
|
|
173
|
-
console.log(
|
|
174
|
-
`[lpm] Install manually from https://github.com/lpm-dev/rust-client/releases/v${version}`,
|
|
175
|
-
)
|
|
176
|
-
console.log(
|
|
177
|
-
"[lpm] Or set LPM_INSTALL_INSECURE=1 to skip integrity verification (NOT recommended).",
|
|
178
|
-
)
|
|
179
|
-
} else {
|
|
180
|
-
console.log(
|
|
181
|
-
`[lpm] ERROR: could not fetch signed manifest: ${err.message}`,
|
|
182
|
-
)
|
|
183
|
-
}
|
|
184
|
-
return null
|
|
185
|
-
}
|
|
186
|
-
try {
|
|
187
|
-
bundleBody = await downloadToBufferFn(bundleUrl, BUNDLE_MAX_BYTES)
|
|
188
|
-
} catch {
|
|
189
|
-
// Bundle 404 / fetch failure is not an attack signal by itself —
|
|
190
|
-
// the SHA-256 gate against the manifest still runs. cosign /
|
|
191
|
-
// sigstore verification is opportunistic on this channel.
|
|
192
|
-
bundleBody = null
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (bundleBody) {
|
|
196
|
-
const verdict = await verifyFn(manifestBody, bundleBody)
|
|
197
|
-
if (verdict === "verified") {
|
|
198
|
-
console.log(
|
|
199
|
-
"[lpm] Verified Sigstore signature on manifest (identity-pinned to release.yml)",
|
|
200
|
-
)
|
|
201
|
-
} else if (verdict === "rejected") {
|
|
202
|
-
// Verifier ran and refused — strong attack signal.
|
|
203
|
-
console.log(
|
|
204
|
-
"[lpm] ERROR: Sigstore verifier rejected the manifest signature.",
|
|
205
|
-
)
|
|
206
|
-
console.log(
|
|
207
|
-
"[lpm] Refusing to install. See https://github.com/lpm-dev/rust-client/issues",
|
|
208
|
-
)
|
|
209
|
-
return null
|
|
210
|
-
} else {
|
|
211
|
-
// Optional `sigstore` package not resolvable — degrade to
|
|
212
|
-
// SHA-only with a loud WARN so the user knows what to
|
|
213
|
-
// install for the stronger posture.
|
|
214
|
-
console.log(
|
|
215
|
-
"[lpm] WARN: optional 'sigstore' package not available; falling back to SHA-256 against the signed manifest only.",
|
|
216
|
-
)
|
|
217
|
-
console.log(
|
|
218
|
-
"[lpm] For Sigstore verification, run `npm install --include=optional` or unblock the sigstore package.",
|
|
219
|
-
)
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const destName = process.platform === "win32" ? "lpm-bin.exe" : "lpm-bin"
|
|
225
|
-
const destPath = path.join(destDir, destName)
|
|
226
|
-
const tmpPath = `${destPath}.tmp.${process.pid}`
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
console.log("[lpm] Downloading native binary from GitHub Releases...")
|
|
230
|
-
await downloadFn(assetUrl, tmpPath, 0, ASSET_MAX_BYTES)
|
|
231
|
-
} catch (err) {
|
|
232
|
-
console.log(`[lpm] Download failed: ${err.message}. Using JS CLI fallback.`)
|
|
233
|
-
safeUnlink(tmpPath)
|
|
234
|
-
return null
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (manifestBody) {
|
|
238
|
-
const actualSha = await sha256FileFn(tmpPath)
|
|
239
|
-
const expectedSha = parseManifestEntry(
|
|
240
|
-
manifestBody.toString("utf-8"),
|
|
241
|
-
githubBinaryName,
|
|
242
|
-
)
|
|
243
|
-
if (!expectedSha) {
|
|
244
|
-
console.log(
|
|
245
|
-
`[lpm] ERROR: manifest does not enumerate ${githubBinaryName}; release-pipeline bug.`,
|
|
246
|
-
)
|
|
247
|
-
safeUnlink(tmpPath)
|
|
248
|
-
return null
|
|
249
|
-
}
|
|
250
|
-
if (actualSha !== expectedSha) {
|
|
251
|
-
console.log(`[lpm] ERROR: SHA-256 mismatch for ${githubBinaryName}`)
|
|
252
|
-
console.log(
|
|
253
|
-
`[lpm] expected: ${expectedSha} (from signed manifest)`,
|
|
254
|
-
)
|
|
255
|
-
console.log(`[lpm] actual: ${actualSha}`)
|
|
256
|
-
console.log("[lpm] Refusing to install — strong tampering signal.")
|
|
257
|
-
safeUnlink(tmpPath)
|
|
258
|
-
return null
|
|
259
|
-
}
|
|
260
|
-
console.log(`[lpm] Verified SHA-256: ${actualSha}`)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
fs.renameSync(tmpPath, destPath)
|
|
265
|
-
fs.chmodSync(destPath, 0o755)
|
|
266
|
-
} catch (err) {
|
|
267
|
-
console.log(`[lpm] Could not install verified binary: ${err.message}`)
|
|
268
|
-
safeUnlink(tmpPath)
|
|
269
|
-
return null
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
console.log("[lpm] Native binary installed from GitHub Releases.")
|
|
273
|
-
return destPath
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function tryHardLink(binaryPath) {
|
|
277
|
-
const shimPath = path.join(binDir, "lpm.js")
|
|
278
|
-
const backupPath = path.join(binDir, "lpm.js.bak")
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
if (fs.existsSync(shimPath) && !fs.existsSync(backupPath)) {
|
|
282
|
-
fs.copyFileSync(shimPath, backupPath)
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (fs.existsSync(shimPath)) {
|
|
286
|
-
fs.unlinkSync(shimPath)
|
|
287
|
-
}
|
|
288
|
-
fs.linkSync(binaryPath, shimPath)
|
|
289
|
-
|
|
290
|
-
console.log("[lpm] Hard-linked native binary (zero Node.js overhead)")
|
|
291
|
-
} catch (err) {
|
|
292
|
-
if (fs.existsSync(backupPath) && !fs.existsSync(shimPath)) {
|
|
293
|
-
fs.copyFileSync(backupPath, shimPath)
|
|
294
|
-
}
|
|
295
|
-
console.log(`[lpm] Hard-link failed (${err.code}). Using JS wrapper.`)
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function tryCopyBinary(binaryPath) {
|
|
300
|
-
const destPath = path.join(binDir, "lpm-bin.exe")
|
|
301
|
-
try {
|
|
302
|
-
fs.copyFileSync(binaryPath, destPath)
|
|
303
|
-
console.log("[lpm] Copied native binary to bin/")
|
|
304
|
-
} catch (err) {
|
|
305
|
-
console.log(`[lpm] Copy failed: ${err.message}. Using JS wrapper.`)
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Download a file from a URL, following redirects (GitHub → S3).
|
|
311
|
-
*
|
|
312
|
-
* HTTPS-only end-to-end: the entry check rejects any non-`https://`
|
|
313
|
-
* URL, and redirect Locations are resolved against the current URL
|
|
314
|
-
* before re-entering the function — so an attacker-controlled mirror
|
|
315
|
-
* cannot downgrade transport mid-chain via `Location: http://…`.
|
|
316
|
-
*
|
|
317
|
-
* Streams to disk with a per-response byte cap so a compromised mirror
|
|
318
|
-
* cannot exhaust disk by serving an unbounded body. The 5-redirect cap
|
|
319
|
-
* is preserved from the pre-integrity-gate shape.
|
|
320
|
-
*
|
|
321
|
-
* Exported for testability.
|
|
322
|
-
*/
|
|
323
|
-
export function download(url, dest, redirects = 0, maxBytes = ASSET_MAX_BYTES) {
|
|
324
|
-
if (redirects > 5) return Promise.reject(new Error("Too many redirects"))
|
|
325
|
-
if (!url.startsWith("https://")) {
|
|
326
|
-
return Promise.reject(
|
|
327
|
-
new Error(
|
|
328
|
-
`refusing non-HTTPS transport: ${url} (Tier-2 download requires HTTPS end-to-end)`,
|
|
329
|
-
),
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return new Promise((resolve, reject) => {
|
|
334
|
-
const req = https.get(url, res => {
|
|
335
|
-
if (
|
|
336
|
-
res.statusCode >= 300 &&
|
|
337
|
-
res.statusCode < 400 &&
|
|
338
|
-
res.headers.location
|
|
339
|
-
) {
|
|
340
|
-
res.resume()
|
|
341
|
-
let nextUrl
|
|
342
|
-
try {
|
|
343
|
-
nextUrl = new URL(res.headers.location, url).href
|
|
344
|
-
} catch {
|
|
345
|
-
reject(
|
|
346
|
-
new Error(`invalid redirect Location: ${res.headers.location}`),
|
|
347
|
-
)
|
|
348
|
-
return
|
|
349
|
-
}
|
|
350
|
-
resolve(download(nextUrl, dest, redirects + 1, maxBytes))
|
|
351
|
-
return
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (res.statusCode !== 200) {
|
|
355
|
-
const err = new Error(`HTTP ${res.statusCode}`)
|
|
356
|
-
err.statusCode = res.statusCode
|
|
357
|
-
res.resume()
|
|
358
|
-
reject(err)
|
|
359
|
-
return
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const advertised = Number(res.headers["content-length"])
|
|
363
|
-
if (Number.isFinite(advertised) && advertised > maxBytes) {
|
|
364
|
-
res.resume()
|
|
365
|
-
reject(
|
|
366
|
-
new Error(
|
|
367
|
-
`response advertises ${advertised} bytes; cap of ${maxBytes} would be exceeded`,
|
|
368
|
-
),
|
|
369
|
-
)
|
|
370
|
-
return
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
let received = 0
|
|
374
|
-
const out = fs.createWriteStream(dest)
|
|
375
|
-
res.on("data", chunk => {
|
|
376
|
-
received += chunk.length
|
|
377
|
-
if (received > maxBytes) {
|
|
378
|
-
out.destroy()
|
|
379
|
-
res.destroy()
|
|
380
|
-
safeUnlink(dest)
|
|
381
|
-
reject(
|
|
382
|
-
new Error(
|
|
383
|
-
`response body exceeded cap of ${maxBytes} bytes mid-stream`,
|
|
384
|
-
),
|
|
385
|
-
)
|
|
386
|
-
}
|
|
387
|
-
})
|
|
388
|
-
res.pipe(out)
|
|
389
|
-
out.on("finish", () => {
|
|
390
|
-
out.close()
|
|
391
|
-
resolve()
|
|
392
|
-
})
|
|
393
|
-
out.on("error", reject)
|
|
394
|
-
})
|
|
395
|
-
req.on("error", reject)
|
|
396
|
-
})
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
/**
|
|
400
|
-
* Buffered variant of [`download`] — fetches a bounded body into memory
|
|
401
|
-
* and returns it as a Buffer. Used for the manifest + Sigstore bundle,
|
|
402
|
-
* both of which are small and must be inspected as a whole before any
|
|
403
|
-
* other byte is honored.
|
|
404
|
-
*
|
|
405
|
-
* HTTPS-only: identical transport-hardening posture as [`download`].
|
|
406
|
-
*
|
|
407
|
-
* Exported for testability.
|
|
408
|
-
*/
|
|
409
|
-
export function downloadToBuffer(url, maxBytes, redirects = 0) {
|
|
410
|
-
if (redirects > 5) return Promise.reject(new Error("Too many redirects"))
|
|
411
|
-
if (!url.startsWith("https://")) {
|
|
412
|
-
return Promise.reject(
|
|
413
|
-
new Error(
|
|
414
|
-
`refusing non-HTTPS transport: ${url} (Tier-2 download requires HTTPS end-to-end)`,
|
|
415
|
-
),
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
return new Promise((resolve, reject) => {
|
|
420
|
-
const req = https.get(url, res => {
|
|
421
|
-
if (
|
|
422
|
-
res.statusCode >= 300 &&
|
|
423
|
-
res.statusCode < 400 &&
|
|
424
|
-
res.headers.location
|
|
425
|
-
) {
|
|
426
|
-
res.resume()
|
|
427
|
-
let nextUrl
|
|
428
|
-
try {
|
|
429
|
-
nextUrl = new URL(res.headers.location, url).href
|
|
430
|
-
} catch {
|
|
431
|
-
reject(
|
|
432
|
-
new Error(`invalid redirect Location: ${res.headers.location}`),
|
|
433
|
-
)
|
|
434
|
-
return
|
|
435
|
-
}
|
|
436
|
-
resolve(downloadToBuffer(nextUrl, maxBytes, redirects + 1))
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (res.statusCode !== 200) {
|
|
441
|
-
const err = new Error(`HTTP ${res.statusCode}`)
|
|
442
|
-
err.statusCode = res.statusCode
|
|
443
|
-
res.resume()
|
|
444
|
-
reject(err)
|
|
445
|
-
return
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const advertised = Number(res.headers["content-length"])
|
|
449
|
-
if (Number.isFinite(advertised) && advertised > maxBytes) {
|
|
450
|
-
res.resume()
|
|
451
|
-
reject(
|
|
452
|
-
new Error(
|
|
453
|
-
`response advertises ${advertised} bytes; cap of ${maxBytes} would be exceeded`,
|
|
454
|
-
),
|
|
455
|
-
)
|
|
456
|
-
return
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
const chunks = []
|
|
460
|
-
let received = 0
|
|
461
|
-
res.on("data", chunk => {
|
|
462
|
-
received += chunk.length
|
|
463
|
-
if (received > maxBytes) {
|
|
464
|
-
res.destroy()
|
|
465
|
-
reject(
|
|
466
|
-
new Error(
|
|
467
|
-
`response body exceeded cap of ${maxBytes} bytes mid-stream`,
|
|
468
|
-
),
|
|
469
|
-
)
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
chunks.push(chunk)
|
|
473
|
-
})
|
|
474
|
-
res.on("end", () => resolve(Buffer.concat(chunks)))
|
|
475
|
-
res.on("error", reject)
|
|
476
|
-
})
|
|
477
|
-
req.on("error", reject)
|
|
478
|
-
})
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Compute the SHA-256 of a file as a lowercase hex string, streaming
|
|
483
|
-
* the file so we never buffer the whole asset in memory.
|
|
484
|
-
*
|
|
485
|
-
* Exported for testability.
|
|
486
|
-
*/
|
|
487
|
-
export function sha256File(filePath) {
|
|
488
|
-
return new Promise((resolve, reject) => {
|
|
489
|
-
const hash = crypto.createHash("sha256")
|
|
490
|
-
const stream = fs.createReadStream(filePath)
|
|
491
|
-
stream.on("data", chunk => hash.update(chunk))
|
|
492
|
-
stream.on("end", () => resolve(hash.digest("hex")))
|
|
493
|
-
stream.on("error", reject)
|
|
494
|
-
})
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Look up the SHA-256 for `filename` in a parsed SHA256SUMS.txt body.
|
|
499
|
-
*
|
|
500
|
-
* Format: `<64-hex> <filename>\n` per line, two ASCII spaces between
|
|
501
|
-
* digest and name (GNU coreutils default; macOS shasum -a 256 matches).
|
|
502
|
-
* Malformed lines return null — a malformed manifest is an integrity
|
|
503
|
-
* failure, not a silent miss; callers should refuse to install.
|
|
504
|
-
*
|
|
505
|
-
* Exported for testability.
|
|
506
|
-
*/
|
|
507
|
-
export function parseManifestEntry(manifestText, filename) {
|
|
508
|
-
const lines = manifestText.split("\n")
|
|
509
|
-
for (const raw of lines) {
|
|
510
|
-
const line = raw.replace(/\r$/, "")
|
|
511
|
-
if (line.length === 0) continue
|
|
512
|
-
const idx = line.indexOf(" ")
|
|
513
|
-
if (idx !== 64) return null
|
|
514
|
-
const digest = line.slice(0, 64).toLowerCase()
|
|
515
|
-
if (!/^[0-9a-f]{64}$/.test(digest)) return null
|
|
516
|
-
const name = line.slice(idx + 2).trim()
|
|
517
|
-
if (name.length === 0) return null
|
|
518
|
-
if (name === filename) return digest
|
|
519
|
-
}
|
|
520
|
-
return null
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
function safeUnlink(p) {
|
|
524
|
-
try {
|
|
525
|
-
if (fs.existsSync(p)) fs.unlinkSync(p)
|
|
526
|
-
} catch {
|
|
527
|
-
// Best-effort.
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Verify a Sigstore bundle against a manifest body using the optional
|
|
533
|
-
* `sigstore` package. Returns:
|
|
534
|
-
*
|
|
535
|
-
* "verified" — bundle verified under the lpm-dev/rust-client
|
|
536
|
-
* release.yml identity pin.
|
|
537
|
-
* "rejected" — sigstore package present and refused the bundle.
|
|
538
|
-
* "unavailable" — sigstore package not resolvable (--no-optional,
|
|
539
|
-
* firewalled, install failure). Caller degrades to
|
|
540
|
-
* SHA-only.
|
|
541
|
-
*
|
|
542
|
-
* Exported for testability.
|
|
543
|
-
*/
|
|
544
|
-
export async function tryVerifyManifestSignature(manifestBody, bundleBody) {
|
|
545
|
-
let sigstore
|
|
546
|
-
try {
|
|
547
|
-
sigstore = await import("sigstore")
|
|
548
|
-
} catch {
|
|
549
|
-
return "unavailable"
|
|
550
|
-
}
|
|
551
|
-
try {
|
|
552
|
-
const bundle = JSON.parse(bundleBody.toString("utf-8"))
|
|
553
|
-
await sigstore.verify(bundle, manifestBody, SIGSTORE_IDENTITY)
|
|
554
|
-
return "verified"
|
|
555
|
-
} catch {
|
|
556
|
-
return "rejected"
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
// Only run `main` when this module is executed as a script (the
|
|
561
|
-
// postinstall hook). Test files that `import { tryGitHubDownload, … }`
|
|
562
|
-
// from this module should not trigger a real install attempt during
|
|
563
|
-
// suite setup.
|
|
564
|
-
const invokedAsScript =
|
|
565
|
-
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href
|
|
566
|
-
|
|
567
|
-
if (invokedAsScript) {
|
|
568
|
-
main().catch(() => {
|
|
569
|
-
// Silently fail — JS CLI fallback will handle all commands.
|
|
570
|
-
})
|
|
571
|
-
}
|