netxops/scripts/build-integrity.mjs
oliver 9b2166e5b6
Some checks failed
Release package / package (macos-latest) (push) Has been cancelled
Release package / package (ubuntu-latest) (push) Has been cancelled
Release package / package (windows-latest) (push) Has been cancelled
Harden public package installation and release validation
2026-10-11 13:32:36 +08:00

44 lines
2.7 KiB
JavaScript

/** 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
}