verify-runtime-manifest.mjs
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠ breaking
25 days ago
| 1 | #!/usr/bin/env node |
| 2 | import crypto from 'node:crypto'; |
| 3 | import fs from 'node:fs'; |
| 4 | import path from 'node:path'; |
| 5 | |
| 6 | function option(name) { |
| 7 | const index = process.argv.indexOf(`--${name}`); |
| 8 | if (index < 0 || !process.argv[index + 1]) throw new Error(`missing --${name}`); |
| 9 | return path.resolve(process.argv[index + 1]); |
| 10 | } |
| 11 | |
| 12 | function sha256(file) { |
| 13 | return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); |
| 14 | } |
| 15 | |
| 16 | function walk(root, relative = '') { |
| 17 | const entries = []; |
| 18 | for (const name of fs.readdirSync(path.join(root, relative)).sort()) { |
| 19 | const rel = relative ? `${relative}/${name}` : name; |
| 20 | if (rel === 'runtime-manifest.json') continue; |
| 21 | const absolute = path.join(root, rel); |
| 22 | const stat = fs.lstatSync(absolute); |
| 23 | if (stat.isSymbolicLink()) throw new Error(`symbolic link in runtime closure: ${rel}`); |
| 24 | if (stat.isDirectory()) entries.push(...walk(root, rel)); |
| 25 | else if (stat.isFile()) entries.push({ |
| 26 | path: rel, |
| 27 | sha256: sha256(absolute), |
| 28 | bytes: stat.size, |
| 29 | executable: (stat.mode & 0o111) !== 0, |
| 30 | }); |
| 31 | else throw new Error(`unsupported runtime entry: ${rel}`); |
| 32 | } |
| 33 | return entries; |
| 34 | } |
| 35 | |
| 36 | const app = option('app'); |
| 37 | const resources = path.join(app, 'Contents/Resources'); |
| 38 | const manifest = JSON.parse(fs.readFileSync(path.join(resources, 'runtime-manifest.json'), 'utf8')); |
| 39 | if (manifest.schema !== 1 || manifest.releaseVersion !== '0.1.0' || manifest.releaseBuild !== 100 || |
| 40 | manifest.architecture !== 'arm64') { |
| 41 | throw new Error('runtime manifest release identity mismatch'); |
| 42 | } |
| 43 | if (process.argv.includes('--require-committed-source') && manifest.sourceRevision?.status !== 'committed') { |
| 44 | throw new Error('runtime manifest is not bound to committed Muse and Git revisions'); |
| 45 | } |
| 46 | const lockFile = path.join(resources, 'release-lock.json'); |
| 47 | const lock = JSON.parse(fs.readFileSync(lockFile, 'utf8')); |
| 48 | if (manifest.releaseLockSHA256 !== sha256(lockFile) || manifest.w3BundleDigest !== lock.model.w3BundleDigest) { |
| 49 | throw new Error('runtime manifest release lock mismatch'); |
| 50 | } |
| 51 | const actual = walk(resources); |
| 52 | const fields = (entry) => [entry.path, entry.sha256, entry.bytes, entry.executable]; |
| 53 | if (!Array.isArray(manifest.files) || |
| 54 | JSON.stringify(actual.map(fields)) !== JSON.stringify(manifest.files.map(fields))) { |
| 55 | throw new Error('runtime manifest file closure mismatch'); |
| 56 | } |
| 57 | process.stdout.write(`runtime manifest verified (${actual.length} files)\n`); |
File History
1 commit
sha256:bd1eb24434d281fe65142038c1bf217493421f5a86880c22dff7e6037d2d4700
feat(companion): add production macOS distribution path
Human
minor
⚠
25 days ago