verify-packed-install.mjs 11 KB

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