diff --git a/packages/server/src/server/plugins/managed-source.posix.test.ts b/packages/server/src/server/plugins/managed-source.posix.test.ts index 7b694f1ef3..440ef52074 100644 --- a/packages/server/src/server/plugins/managed-source.posix.test.ts +++ b/packages/server/src/server/plugins/managed-source.posix.test.ts @@ -575,3 +575,53 @@ it("installs and updates only the npm artifacts pinned by the plugin registry", else process.env.npm_config_userconfig = previous; } }, 30_000); + +it("installs a registry npm pin through the configured npm mirror", async () => { + const { startNpmRegistry, npmPluginPackages } = + await import("../../../../../scripts/test-support/npm-registry.mjs"); + const { readNpmArtifact, resolveNpm } = await import("./managed-source/npm.js"); + const mirror = await startNpmRegistry(npmPluginPackages()); + const previous = process.env.npm_config_userconfig; + process.env.npm_config_userconfig = mirror.userconfig; + // The public registry pins npmjs.org tarball URLs, which a mirror does not serve at the same path. + const pin = { + version: "1.0.0", + resolved: "https://registry.npmjs.org/paseo-fixture-plugin/-/paseo-fixture-plugin-1.0.0.tgz", + integrity: "", + }; + const server = createServer((_request, response) => { + response.end( + JSON.stringify({ + id: "acme/example", + artifact: { kind: "npm", package: "paseo-fixture-plugin", ...pin }, + }), + ); + }); + try { + const home = await mkdtemp(path.join(tmpdir(), "paseo-registry-npm-mirror-")); + roots.push(home); + pin.integrity = (await resolveNpm("paseo-fixture-plugin", "1.0.0", home)).integrity; + await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve)); + const address = server.address(); + if (!address || typeof address === "string") throw new Error("Missing registry address"); + const sources = new ManagedPluginSources(home, { + defaultUrl: `http://127.0.0.1:${address.port}`, + }); + const candidate = await sources.place( + "example", + await sources.prepareInstall({ source: "acme/example" }), + ); + await sources.verifyCandidate("example", candidate); + sources.commit("example", candidate.record); + expect((await sources.describe("example", candidate.directory)).currentRevision).toBe("1.0.0"); + expect( + await readNpmArtifact(path.resolve(candidate.directory, "../.."), "paseo-fixture-plugin"), + ).toEqual({ ...pin, resolved: `${mirror.url}/artifacts/1.tgz` }); + } finally { + server.closeAllConnections(); + await new Promise((resolve) => server.close(() => resolve())); + await mirror.close(); + if (previous === undefined) delete process.env.npm_config_userconfig; + else process.env.npm_config_userconfig = previous; + } +}, 30_000); diff --git a/packages/server/src/server/plugins/managed-source.ts b/packages/server/src/server/plugins/managed-source.ts index 5373b7453a..a20be890b3 100644 --- a/packages/server/src/server/plugins/managed-source.ts +++ b/packages/server/src/server/plugins/managed-source.ts @@ -22,7 +22,13 @@ import { import { runGitCommand } from "../../utils/run-git-command.js"; import { ensurePrivateDirectory, writePrivateFileAtomicSync } from "../private-files.js"; import { readPluginManifest, type PluginManifest } from "./manifest.js"; -import { acquireNpm, isNpmSource, readNpmArtifact, resolveNpm } from "./managed-source/npm.js"; +import { + acquireNpm, + isNpmSource, + isSameNpmArtifact, + readNpmArtifact, + resolveNpm, +} from "./managed-source/npm.js"; const GIT_TIMEOUT_MS = 120_000; const GIT_ENV = { GIT_TERMINAL_PROMPT: "0" } as const; @@ -346,11 +352,7 @@ export class ManagedPluginSources { throw new Error("Preparation changed the reviewed Git commit"); if (target?.kind === "npm" && location.identity.kind === "npm") { const artifact = await readNpmArtifact(location.versionRoot, location.identity.packageName); - if ( - artifact.version !== target.version || - artifact.integrity !== target.integrity || - artifact.resolved !== target.resolved - ) + if (!isSameNpmArtifact({ installed: artifact, target })) throw new Error("Preparation changed the reviewed npm artifact"); } } diff --git a/packages/server/src/server/plugins/managed-source/npm.ts b/packages/server/src/server/plugins/managed-source/npm.ts index 315bbc981e..419a807201 100644 --- a/packages/server/src/server/plugins/managed-source/npm.ts +++ b/packages/server/src/server/plugins/managed-source/npm.ts @@ -48,7 +48,7 @@ export async function acquireNpm(source: string, installRoot: string, target?: N name: "paseo-plugin-installation", version: "1.0.0", private: true, - dependencies: { [packageName]: target?.resolved ?? requestedSpec }, + dependencies: { [packageName]: requestedSpec }, }), ); const npm = await requireNpm(); @@ -70,23 +70,28 @@ export async function acquireNpm(source: string, installRoot: string, target?: N { cwd: installRoot, timeout: 120_000, maxBuffer: 1024 * 1024 }, ); const installed = await readNpmArtifact(installRoot, packageName); - if ( - target && - (installed.version !== target.version || - installed.integrity !== target.integrity || - installed.resolved !== target.resolved) - ) { + if (target && !isSameNpmArtifact({ installed, target })) { throw new Error(`npm artifact changed since review for ${packageName}`); } return { packageName, ...installed }; } +// Mirrors serve the reviewed tarball from their own URL, so the content hash identifies it. +export function isSameNpmArtifact({ installed, target }: NpmArtifactComparison): boolean { + return installed.version === target.version && installed.integrity === target.integrity; +} + export interface NpmArtifact { version: string; resolved: string; integrity: string; } +export interface NpmArtifactComparison { + installed: NpmArtifact; + target: NpmArtifact; +} + export async function readNpmArtifact( installRoot: string, packageName: string,