verify-packed-install.mjs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #!/usr/bin/env node
  2. /**
  3. * Publish-path rehearsal without publishing: verify the packed tarballs are
  4. * exactly what a consumer install needs. `pnpm pack` already produced the
  5. * bytes `pnpm publish` would upload; this script checks the payload
  6. * (coverage, concrete dependency versions, NO lifecycle install scripts —
  7. * this family has no install fallback on purpose), unpacks the entry plus
  8. * THIS host's platform tarball into a throwaway consumer OUTSIDE the repo,
  9. * byte-pins the installed binary against the workspace build it was packed
  10. * from, and drives the INSTALLED entry under plain `node` — resolution,
  11. * probe, and a real confinement world-proof through the installed launcher.
  12. *
  13. * On non-Linux hosts (no platform package exists) it instead proves the
  14. * documented degradation: resolution falls back to a nonexistent path and
  15. * the probe reports `unusable`.
  16. *
  17. * Usage: `node scripts/verify-packed-install.mjs [tarball-dir] [--current-platform-only]`.
  18. * The flag skips the all-platforms tarball-presence check for
  19. * per-architecture CI legs. `NALR_REQUIRE_LANDLOCK=1` makes an unenforcing
  20. * kernel a failure instead of a skipped world-proof (set on CI, where the
  21. * kernel is known).
  22. */
  23. import crypto from 'node:crypto';
  24. import fs from 'node:fs';
  25. import os from 'node:os';
  26. import path from 'node:path';
  27. import { spawnSync } from 'node:child_process';
  28. import { entryDirs, packageDirs, platformDirs, readJson, root } from './repo.mjs';
  29. const args = process.argv.slice(2);
  30. const currentPlatformOnly = args.includes('--current-platform-only');
  31. const tarballDir = path.resolve(args.find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
  32. const entryPackageName = '@deepseek-ai/node-addon-landlock-run';
  33. function tarballName(manifest) {
  34. if (manifest.name.startsWith('@')) {
  35. return `${manifest.name.slice(1).replace('/', '-')}-${manifest.version}.tgz`;
  36. }
  37. return `${manifest.name}-${manifest.version}.tgz`;
  38. }
  39. function tarballPath(manifest) {
  40. const tarball = path.join(tarballDir, tarballName(manifest));
  41. if (!fs.existsSync(tarball)) {
  42. throw new Error(`missing packed tarball: ${tarball}`);
  43. }
  44. return tarball;
  45. }
  46. function run(command, commandArgs, options = {}) {
  47. const result = spawnSync(command, commandArgs, {
  48. cwd: options.cwd || root,
  49. stdio: 'inherit',
  50. env: { ...process.env, ...options.env },
  51. });
  52. if (result.error) throw result.error;
  53. if (result.status !== 0) {
  54. process.exit(result.status ?? 1);
  55. }
  56. }
  57. function runCapture(command, commandArgs) {
  58. const result = spawnSync(command, commandArgs, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
  59. if (result.error) throw result.error;
  60. if (result.status !== 0) {
  61. process.stderr.write(result.stderr);
  62. process.exit(result.status ?? 1);
  63. }
  64. return result.stdout;
  65. }
  66. function readPackedManifest(manifest) {
  67. return JSON.parse(runCapture('tar', ['-xOf', tarballPath(manifest), 'package/package.json']));
  68. }
  69. function verifyPackedManifest(packed) {
  70. const lifecycle = ['preinstall', 'install', 'postinstall', 'prepare'];
  71. for (const script of lifecycle) {
  72. if (packed.scripts?.[script]) {
  73. throw new Error(`${packed.name}: packed manifest carries a "${script}" lifecycle script — this family has no install fallback`);
  74. }
  75. }
  76. for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies']) {
  77. for (const [name, version] of Object.entries(packed[field] ?? {})) {
  78. if (version.includes('workspace:')) {
  79. throw new Error(`${packed.name}: packed ${field} still uses the workspace protocol: ${name}@${version}`);
  80. }
  81. }
  82. }
  83. }
  84. function sha256(file) {
  85. return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
  86. }
  87. function packageInstallDir(packageName) {
  88. return path.join(tempRoot, 'node_modules', ...packageName.split('/'));
  89. }
  90. function unpackTarball(manifest) {
  91. const extractRoot = fs.mkdtempSync(path.join(tempRoot, 'extract-'));
  92. run('tar', ['-xzf', tarballPath(manifest), '-C', extractRoot]);
  93. const source = path.join(extractRoot, 'package');
  94. const destination = packageInstallDir(manifest.name);
  95. fs.rmSync(destination, { recursive: true, force: true });
  96. fs.mkdirSync(path.dirname(destination), { recursive: true });
  97. fs.renameSync(source, destination);
  98. fs.rmSync(extractRoot, { recursive: true, force: true });
  99. console.log(`Unpacked ${manifest.name} -> ${path.relative(tempRoot, destination)}`);
  100. }
  101. const manifests = packageDirs().map((dir) => ({ dir, manifest: readJson(path.join(root, dir, 'package.json')) }));
  102. const entryManifest = manifests.find(({ manifest }) => manifest.name === entryPackageName)?.manifest;
  103. if (!entryManifest) throw new Error(`missing source manifest for ${entryPackageName}`);
  104. const hostPlatform = `${process.platform}-${process.arch}`;
  105. const currentPlatformEntry = manifests.find(
  106. ({ dir, manifest }) => platformDirs().includes(dir) && manifest.name === `${entryPackageName}-${hostPlatform}`,
  107. );
  108. // Payload checks: every expected tarball exists (full mode), the packed
  109. // entry's optional-dependency set names exactly the platform packages, and
  110. // no packed manifest carries workspace versions or install lifecycle.
  111. const expectedTarballs = currentPlatformOnly
  112. ? manifests.filter(({ dir }) => entryDirs().includes(dir) || dir === currentPlatformEntry?.dir)
  113. : manifests;
  114. for (const { manifest } of expectedTarballs) {
  115. tarballPath(manifest);
  116. }
  117. const packedEntry = readPackedManifest(entryManifest);
  118. const platformPackageNames = manifests
  119. .filter(({ dir }) => platformDirs().includes(dir))
  120. .map(({ manifest }) => manifest.name)
  121. .sort();
  122. const optionalNames = Object.keys(packedEntry.optionalDependencies || {}).sort();
  123. if (optionalNames.join('\n') !== platformPackageNames.join('\n')) {
  124. throw new Error(`packed entry optionalDependencies mismatch\nactual:\n${optionalNames.join('\n')}\nexpected:\n${platformPackageNames.join('\n')}`);
  125. }
  126. for (const { manifest } of expectedTarballs) {
  127. verifyPackedManifest(readPackedManifest(manifest));
  128. }
  129. // Throwaway ESM consumer, built from local tarballs only — no registry.
  130. const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-packed-install-'));
  131. fs.writeFileSync(
  132. path.join(tempRoot, 'package.json'),
  133. `${JSON.stringify({ name: 'nalr-packed-install-check', version: '0.0.0', private: true, type: 'module' }, null, 2)}\n`,
  134. );
  135. console.log(`Verifying packed install in ${tempRoot}`);
  136. unpackTarball(entryManifest);
  137. if (currentPlatformEntry) {
  138. unpackTarball(currentPlatformEntry.manifest);
  139. // Byte-pin: the installed binary must be the workspace build it was packed
  140. // from — any divergence means the tarball did not carry the built bytes.
  141. const prebuilds = readJson(path.join(root, currentPlatformEntry.dir, 'prebuilds.json'));
  142. for (const binary of prebuilds.binaries) {
  143. const workspaceFile = path.join(root, currentPlatformEntry.dir, binary.path);
  144. const installedFile = path.join(packageInstallDir(currentPlatformEntry.manifest.name), binary.path);
  145. if (sha256(workspaceFile) !== sha256(installedFile)) {
  146. throw new Error(`installed ${binary.path} differs from the workspace build it was packed from`);
  147. }
  148. console.log(`Byte-pinned ${binary.path} against the workspace build`);
  149. }
  150. } else if (process.platform === 'linux') {
  151. throw new Error(`linux host without a platform package in the matrix: ${hostPlatform}`);
  152. }
  153. // Drive the INSTALLED entry under plain node: resolution, probe, and (on an
  154. // enforcing kernel) a real confinement world-proof through the installed
  155. // launcher.
  156. const driver = path.join(tempRoot, 'driver.mjs');
  157. fs.writeFileSync(driver, `
  158. import assert from 'node:assert/strict';
  159. import { spawnSync } from 'node:child_process';
  160. import fs from 'node:fs';
  161. import os from 'node:os';
  162. import path from 'node:path';
  163. import { grantArgs, launcherPath, probe } from '@deepseek-ai/node-addon-landlock-run';
  164. const requireLandlock = process.env.NALR_REQUIRE_LANDLOCK === '1';
  165. const platformPackage = '@deepseek-ai/node-addon-landlock-run-' + process.platform + '-' + process.arch;
  166. const resolved = launcherPath();
  167. assert.ok(path.isAbsolute(resolved), 'launcherPath must be absolute');
  168. assert.ok(resolved.includes(path.join(...platformPackage.split('/'))), 'launcherPath must point into the platform package: ' + resolved);
  169. if (process.platform === 'linux') {
  170. assert.ok(fs.existsSync(resolved), 'installed launcher missing at ' + resolved);
  171. try {
  172. fs.accessSync(resolved, fs.constants.X_OK);
  173. } catch {
  174. throw new Error('installed launcher is not executable — the pack path stripped the mode bit: ' + resolved);
  175. }
  176. const enforcement = probe(resolved);
  177. console.log('probe through the installed launcher: ' + enforcement);
  178. if (enforcement === 'unusable') {
  179. if (requireLandlock) throw new Error('NALR_REQUIRE_LANDLOCK=1 but the probe reports unusable');
  180. console.log('kernel does not enforce Landlock — skipping the confinement world-proof');
  181. } else {
  182. const work = fs.mkdtempSync(path.join(os.tmpdir(), 'nalr-confine-'));
  183. const denied = path.join(work, 'denied.txt');
  184. const deniedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'] }), '--', '/bin/sh', '-c', 'echo x > ' + denied], { encoding: 'utf8' });
  185. assert.notEqual(deniedRun.status, 0, 'write outside the grants must fail');
  186. assert.ok(!fs.existsSync(denied), 'denied write must not land on disk');
  187. const granted = path.join(work, 'granted.txt');
  188. const grantedRun = spawnSync(resolved, [...grantArgs({ readOnly: ['/'], readWrite: [work] }), '--', '/bin/sh', '-c', 'echo ok > ' + granted], { encoding: 'utf8' });
  189. assert.equal(grantedRun.status, 0, 'granted write must succeed: ' + grantedRun.stderr);
  190. assert.equal(fs.readFileSync(granted, 'utf8').trim(), 'ok');
  191. console.log('confinement world-proof passed through the installed launcher');
  192. }
  193. } else {
  194. assert.ok(!fs.existsSync(resolved), 'no platform package exists for this host — the fallback path must not exist');
  195. assert.equal(probe(resolved), 'unusable');
  196. console.log('non-linux host: fallback resolution and unusable probe verified');
  197. }
  198. `);
  199. run(process.execPath, [driver], { cwd: tempRoot });
  200. console.log('Packed install verification passed.');