mirror of
https://github.com/waeckerlinfederowicz66-sketch/mcp-flowspeech-server.git
synced 2026-11-02 03:01:02 +00:00
214 lines
12 KiB
JavaScript
214 lines
12 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { createHash } from 'node:crypto';
|
|
import { mkdtemp, mkdir, readFile, rm, writeFile, symlink } from 'node:fs/promises';
|
|
import { existsSync } from 'node:fs';
|
|
import net from 'node:net';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { archiveFiles, checkMetadata, npmTools, prepareRelease, registryPayload } from './prepare-private-release.mjs';
|
|
|
|
const root = fileURLToPath(new URL('..', import.meta.url));
|
|
const pkg = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8'));
|
|
const archive = path.resolve(process.env.FLOWSPEECH_RELEASE_ARCHIVE ?? path.join(root, pkg.name + '-' + pkg.version + '.tgz'));
|
|
const npmCli = process.env.FLOWSPEECH_NPM_CLI ?? process.env.npm_execpath;
|
|
const tools = npmTools(npmCli);
|
|
const digest = bytes => createHash('sha256').update(bytes).digest('hex');
|
|
const bytes = await readFile(archive);
|
|
const expectedSha256 = digest(bytes);
|
|
const fixtureRoot = path.join(root, '.release-stage');
|
|
await mkdir(fixtureRoot, { recursive: true });
|
|
|
|
const connect = net.Socket.prototype.connect;
|
|
const fetch = globalThis.fetch;
|
|
net.Socket.prototype.connect = function () { throw new Error('Network forbidden in release privacy tests'); };
|
|
globalThis.fetch = async () => { throw new Error('Network forbidden in release privacy tests'); };
|
|
test.after(() => { net.Socket.prototype.connect = connect; globalThis.fetch = fetch; });
|
|
|
|
for (const field of ['_resolved', '_from', '_where', '_integrity']) {
|
|
test('rejects npm-injected ' + field + ' rather than deleting evidence', () => {
|
|
const value = { name: pkg.name, version: pkg.version, [field]: '/private/fixture/release.tgz' };
|
|
assert.throws(() => checkMetadata(value, tools), /Disallowed npm provenance field/);
|
|
assert.ok(value[field], 'Validation must not erase the original metadata');
|
|
});
|
|
}
|
|
for (const value of ['/Users/fixture/package', '/home/fixture/package', '/opt/fixture/package', 'file:../candidate.tgz', 'C:\\fixture\\package']) {
|
|
test('rejects a private/local manifest path form', () => {
|
|
assert.throws(() => checkMetadata({ dependencies: { fixture: value } }, tools), /Absolute\/local path/);
|
|
});
|
|
}
|
|
test('HTTPS URLs and relative published entrypoints are not mistaken for filesystem paths', () => {
|
|
checkMetadata({ repository: 'https://github.com/example/package.git', main: './dist/index.js' }, tools);
|
|
});
|
|
|
|
test('reproduces tarball metadata leakage and blocks its offline registry payload', async () => {
|
|
const manifest = await tools.require('pacote').manifest(archive, {
|
|
offline: true, fullmetadata: true, fullReadJson: true, cache: path.join(fixtureRoot, 'cache'),
|
|
});
|
|
assert.equal(typeof manifest._resolved, 'string');
|
|
assert.ok(path.isAbsolute(manifest._resolved));
|
|
await assert.rejects(registryPayload(manifest, bytes, tools), /Disallowed npm provenance field/);
|
|
});
|
|
|
|
test('stages exact reviewed bytes, assembles path-free directory metadata and never overwrites a candidate', async () => {
|
|
const target = path.join(fixtureRoot, 'success-' + process.pid);
|
|
try {
|
|
const result = await prepareRelease({ archive, expectedSha256, destination: target, npmCli });
|
|
assert.equal(result.inputSha256, expectedSha256);
|
|
assert.equal(result.directoryPackSha256, expectedSha256);
|
|
assert.equal(result.networkRequests, 0);
|
|
assert.equal(result.publishPerformed, false);
|
|
assert.equal(result.existingRegistryMetadataChanged, false);
|
|
const before = await archiveFiles(archive, tools);
|
|
const after = await archiveFiles(path.join(target, 'candidate.tgz'), tools);
|
|
assert.deepEqual(after.files, before.files);
|
|
assert.deepEqual(await readFile(archive), bytes);
|
|
await assert.rejects(prepareRelease({ archive, expectedSha256, destination: target, npmCli }));
|
|
assert.ok(existsSync(path.join(target, 'privacy-audit.json')), 'Prior evidence must survive a collision');
|
|
} finally { await rm(target, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('fails closed on a wrong digest or unverified npm toolchain', async () => {
|
|
await assert.rejects(prepareRelease({ archive, expectedSha256: '0'.repeat(64), npmCli }), /hash mismatch/);
|
|
const tmp = await mkdtemp(path.join(fixtureRoot, 'npm-fixture-'));
|
|
try {
|
|
await mkdir(path.join(tmp, 'bin'));
|
|
await writeFile(path.join(tmp, 'bin/npm-cli.js'), '');
|
|
await writeFile(path.join(tmp, 'package.json'), JSON.stringify({ name: 'npm', version: '99.0.0' }));
|
|
assert.throws(() => npmTools(path.join(tmp, 'bin/npm-cli.js')), /Unverified npm version/);
|
|
} finally { await rm(tmp, { recursive: true, force: true }); }
|
|
});
|
|
|
|
test('rejects diagnostics, local metadata and links in the candidate archive', async () => {
|
|
const tmp = await mkdtemp(path.join(fixtureRoot, 'archive-fixture-'));
|
|
try {
|
|
await tools.tar.x({ file: archive, cwd: tmp });
|
|
const filename = path.join(tmp, 'bad.tgz');
|
|
const packageRoot = path.join(tmp, 'package');
|
|
await mkdir(path.join(packageRoot, 'dist/post-release'));
|
|
await writeFile(path.join(packageRoot, 'dist/post-release/registry.json'), '{}');
|
|
await tools.tar.c({ gzip: true, portable: true, file: filename, cwd: tmp }, ['package']);
|
|
await assert.rejects(archiveFiles(filename, tools));
|
|
await rm(path.join(packageRoot, 'dist/post-release'), { recursive: true });
|
|
await symlink('index.js', path.join(packageRoot, 'dist/link.js'));
|
|
await tools.tar.c({ gzip: true, portable: true, file: filename, cwd: tmp }, ['package']);
|
|
await assert.rejects(archiveFiles(filename, tools));
|
|
} finally { await rm(tmp, { recursive: true, force: true }); }
|
|
});
|
|
|
|
async function withManifest(changes, run) {
|
|
const tmp = await mkdtemp(path.join(fixtureRoot, 'dependency-fixture-'));
|
|
const destination = tmp + '-stage';
|
|
try {
|
|
await tools.tar.x({ file: archive, cwd: tmp });
|
|
const filename = path.join(tmp, 'candidate.tgz');
|
|
const manifestFile = path.join(tmp, 'package/package.json');
|
|
const manifest = { ...JSON.parse(await readFile(manifestFile, 'utf8')), ...changes };
|
|
await writeFile(manifestFile, JSON.stringify(manifest, null, 2) + '\n');
|
|
await tools.tar.c({ gzip: true, portable: true, file: filename, cwd: tmp }, ['package']);
|
|
await run({ archive: filename, expectedSha256: digest(await readFile(filename)), destination, npmCli }, manifest);
|
|
} finally {
|
|
await rm(tmp, { recursive: true, force: true });
|
|
await rm(destination, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function rejectsManifest(changes) {
|
|
await withManifest(changes, async (options, manifest) => {
|
|
assert.throws(() => checkMetadata(manifest, tools));
|
|
await assert.rejects(registryPayload(manifest, bytes, tools));
|
|
await assert.rejects(prepareRelease(options));
|
|
assert.ok(!existsSync(options.destination), 'Rejected dependency must never receive a clean audit report');
|
|
});
|
|
}
|
|
|
|
const localSpecs = ['../private-project', './private-project', '~/private-project',
|
|
'link:../private-project', 'workspace:*', 'file:../private-project', 'git+file:///opt/private-project'];
|
|
for (const field of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
test('rejects parsed local specs in ' + field + ' before metadata or a full preparation report', async () => {
|
|
for (const spec of localSpecs) await rejectsManifest({ [field]: { fixture: spec } });
|
|
});
|
|
}
|
|
|
|
test('rejects local aliases without echoing parser errors or dependency paths', async () => {
|
|
for (const spec of ['npm:fixture@file:../private-project', 'npm:fixture@link:../private-project', 'npm:fixture@workspace:*']) {
|
|
assert.throws(() => checkMetadata({ dependencies: { fixture: spec } }, tools), error =>
|
|
error.message === 'Local or unsupported dependency specification' && !error.message.includes(spec));
|
|
await rejectsManifest({ dependencies: { fixture: spec } });
|
|
}
|
|
});
|
|
|
|
test('checks nested overrides, package selectors and dependency references structurally', async () => {
|
|
for (const spec of localSpecs) {
|
|
await rejectsManifest({ overrides: { fixture: spec } });
|
|
await rejectsManifest({ overrides: { 'parent@^1': { fixture: { '.': spec } } } });
|
|
}
|
|
await rejectsManifest({ overrides: { 'fixture@file:../private-project': { other: '^1' } } });
|
|
await rejectsManifest({ dependencies: { fixture: '../private-project' }, overrides: { fixture: '$fixture' } });
|
|
await rejectsManifest({ overrides: { fixture: '$missing' } });
|
|
});
|
|
|
|
test('accepts registry versions, ranges, tags, aliases and relative published entrypoints end to end', async () => {
|
|
const manifest = {
|
|
main: './dist/index.js', types: './dist/index.d.ts', bin: { fixture: './dist/index.js' },
|
|
exports: { '.': { types: './dist/index.d.ts', default: './dist/index.js' } },
|
|
dependencies: { fixture: '^1.2.3', exact: '1.2.3', tagged: 'latest', alias: 'npm:other@^2' },
|
|
devDependencies: { scoped: 'npm:@example/package@next' },
|
|
optionalDependencies: { optional: '~1.0.0' }, peerDependencies: { peer: '>=1 <3' },
|
|
overrides: { fixture: '$fixture', 'parent@^1': { '.': '1.2.3', alias: 'npm:other@^2' } },
|
|
};
|
|
checkMetadata(manifest, tools);
|
|
await withManifest(manifest, async (options, candidate) => {
|
|
const payload = await registryPayload(candidate, bytes, tools);
|
|
assert.equal(payload.versions[candidate.version].dependencies.alias, 'npm:other@^2');
|
|
const report = await prepareRelease(options);
|
|
assert.equal(report.dependencySpecsVerified, true);
|
|
assert.equal(report.networkRequests, 0);
|
|
assert.equal(report.privatePathFields, 0);
|
|
const after = await archiveFiles(path.join(options.destination, 'candidate.tgz'), tools);
|
|
assert.deepEqual(after.manifest.exports, manifest.exports);
|
|
assert.deepEqual(after.manifest.bin, manifest.bin);
|
|
});
|
|
});
|
|
|
|
test('fails closed without the pinned parser and rejects malformed dependency collections', () => {
|
|
assert.throws(() => checkMetadata({ dependencies: { fixture: '^1' } }), /Pinned npm dependency parser/);
|
|
for (const value of [null, [], 'fixture', { fixture: null }]) {
|
|
assert.throws(() => checkMetadata({ dependencies: value }, tools));
|
|
}
|
|
});
|
|
|
|
test('rejects credential-bearing URL metadata end to end without echoing credentials', async () => {
|
|
const canary = 'review-credential-canary-only';
|
|
for (const spec of [
|
|
'git+https://review-user:' + canary + '@github.com/example/package.git',
|
|
'https://review-user:' + canary + '@example.test/package.tgz',
|
|
'git+https://' + canary + '@github.com/example/package.git',
|
|
'git+ssh://git:' + canary + '@example.test/package.git',
|
|
'https://example.test/package.tgz?access_token=' + canary,
|
|
]) {
|
|
for (const field of ['dependencies', 'optionalDependencies', 'peerDependencies', 'devDependencies']) {
|
|
await rejectsManifest({ [field]: { fixture: spec } });
|
|
}
|
|
await rejectsManifest({ overrides: { 'parent@^1': { fixture: spec } } });
|
|
await rejectsManifest({ repository: { type: 'git', url: spec } });
|
|
await rejectsManifest({ homepage: spec });
|
|
assert.throws(() => checkMetadata({ dependencies: { fixture: spec } }, tools), error =>
|
|
!error.message.includes(canary) && !error.message.includes('review-user'));
|
|
}
|
|
});
|
|
|
|
test('public Git, conventional SSH and tarball dependencies remain supported', async () => {
|
|
const dependencies = {
|
|
publicgit: 'git+https://github.com/example/package.git',
|
|
shortcut: 'github:example/package',
|
|
ssh: 'git+ssh://git@github.com/example/package.git',
|
|
archive: 'https://example.test/package.tgz',
|
|
};
|
|
await withManifest({ dependencies }, async (options, candidate) => {
|
|
const report = await prepareRelease(options);
|
|
assert.equal(report.networkRequests, 0);
|
|
assert.deepEqual((await registryPayload(candidate, bytes, tools)).versions[candidate.version].dependencies, dependencies);
|
|
});
|
|
});
|