mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-19 23:03:33 +08:00
Harden public package installation and release validation
This commit is contained in:
parent
9271494b37
commit
9b2166e5b6
41 changed files with 994 additions and 363 deletions
44
scripts/build-integrity.mjs
Normal file
44
scripts/build-integrity.mjs
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/** Track the actual bundle inputs so a release cannot silently ship stale JS. */
|
||||
import { createHash } from 'node:crypto'
|
||||
import { readFileSync, writeFileSync } from 'node:fs'
|
||||
import { dirname, isAbsolute, relative, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
export const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const manifestPath = resolve(root, '.release-build.json')
|
||||
export const hashText = text => createHash('sha256').update(text.replace(/\r\n/g, '\n')).digest('hex')
|
||||
export const hashFile = path => hashText(readFileSync(path, 'utf8'))
|
||||
|
||||
export function recordBuild(output, result, buildScript) {
|
||||
if (!result.metafile) throw new Error('Build metadata is missing; use Bun >=1.3.13.')
|
||||
const inputs = [...new Set([
|
||||
'package.json', 'pnpm-lock.yaml', buildScript, 'scripts/build-integrity.mjs',
|
||||
...Object.keys(result.metafile.inputs).map(path => relative(root, resolve(path)).replaceAll('\\', '/'))
|
||||
.filter(path => path.startsWith('src/')),
|
||||
])].sort()
|
||||
if (!inputs.some(path => path.startsWith('src/'))) throw new Error('Bundle has no recorded source inputs.')
|
||||
for (const path of inputs) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Build input escapes repository: ${path}`)
|
||||
}
|
||||
let builds = {}
|
||||
try { builds = JSON.parse(readFileSync(manifestPath, 'utf8')).builds ?? {} } catch { /* First build. */ }
|
||||
const key = relative(root, output).replaceAll('\\', '/')
|
||||
builds[key] = { output: hashFile(output), inputs: Object.fromEntries(inputs.map(path => [path, hashFile(resolve(root, path))])) }
|
||||
writeFileSync(manifestPath, `${JSON.stringify({ format: 1, builds: Object.fromEntries(Object.entries(builds).sort()) }, null, 2)}\n`)
|
||||
}
|
||||
|
||||
export function checkBuilds(directory = root) {
|
||||
const manifest = JSON.parse(readFileSync(resolve(directory, '.release-build.json'), 'utf8'))
|
||||
if (manifest.format !== 1) throw new Error('Unknown build manifest format.')
|
||||
const pkg = JSON.parse(readFileSync(resolve(directory, 'package.json'), 'utf8'))
|
||||
const outputs = [...new Set(Object.values(pkg.exports).filter(path => path.endsWith('.js')).map(path => path.replace(/^\.\//, '')))]
|
||||
for (const output of outputs) {
|
||||
const build = manifest.builds?.[output]
|
||||
if (!build || hashFile(resolve(directory, output)) !== build.output) throw new Error(`Missing or modified artifact: ${output}; run bun run bundle.`)
|
||||
for (const [path, hash] of Object.entries(build.inputs)) {
|
||||
if (isAbsolute(path) || path.startsWith('../')) throw new Error(`Invalid build input: ${path}`)
|
||||
if (hashFile(resolve(directory, path)) !== hash) throw new Error(`Stale artifact ${output}: ${path} changed; run bun run bundle.`)
|
||||
}
|
||||
}
|
||||
return outputs
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue