Skip to content

Commit

Permalink
Merge branch 'main' into separate-r-from-w
Browse files Browse the repository at this point in the history
  • Loading branch information
aduh95 authored Apr 13, 2024
2 parents 38763e5 + c800d3e commit b069c20
Show file tree
Hide file tree
Showing 15 changed files with 439 additions and 66 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ projects, `pnpm install` in pnpm projects, and `npm` in npm projects. Corepack
will catch these calls, and depending on the situation:

- **If the local project is configured for the package manager you're using**,
Corepack will silently download and cache the latest compatible version.
Corepack will download and cache the latest compatible version.

- **If the local project is configured for a different package manager**,
Corepack will request you to run the command again using the right package
Expand Down Expand Up @@ -294,6 +294,9 @@ same major line. Should you need to upgrade to a new major, use an explicit
- `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through
[`node-proxy-agent`](https://github.com/TooTallNate/node-proxy-agent).

- `COREPACK_INTEGRITY_KEYS` can be set to an empty string to instruct Corepack
to skip integrity checks, or a JSON string containing custom keys.

## Troubleshooting

### Networking
Expand Down
11 changes: 11 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,16 @@
}
}
}
},
"keys": {
"npm": [
{
"expires": null,
"keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA",
"keytype": "ecdsa-sha2-nistp256",
"scheme": "ecdsa-sha2-nistp256",
"key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1Olb3zMAFFxXKHiIkQO5cJ3Yhl5i6UPp+IhuteBJbuHcA5UogKo0EWtlWwW6KSaKoTNEYL7JlCQiVnkhBktUgg=="
}
]
}
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@
"proxy-from-env": "^1.1.0",
"semver": "^7.5.2",
"supports-color": "^9.0.0",
"tar": "^6.0.1",
"tar": "^6.2.1",
"ts-node": "^10.0.0",
"typescript": "^5.3.3",
"undici": "^6.6.1",
"v8-compile-cache": "^2.3.0",
"which": "^4.0.0"
},
"resolutions": {
"undici-types": "6.x"
},
"scripts": {
"build": "rm -rf dist shims && run build:bundle && ts-node ./mkshims.ts",
"build:bundle": "esbuild ./sources/_lib.ts --bundle --platform=node --target=node18.17.0 --external:corepack --outfile='./dist/lib/corepack.cjs' --resolve-extensions='.ts,.mjs,.js'",
Expand Down
2 changes: 1 addition & 1 deletion sources/Engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export class Engine {

let isTransparentCommand = false;
if (packageManager != null) {
const defaultVersion = await this.getDefaultVersion(packageManager);
const defaultVersion = binaryVersion || await this.getDefaultVersion(packageManager);
const definition = this.config.definitions[packageManager]!;

// If all leading segments match one of the patterns defined in the `transparent`
Expand Down
17 changes: 15 additions & 2 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,13 +218,15 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}

let url: string;
let signatures: Array<{keyid: string, sig: string}>;
let integrity: string;
let binPath: string | null = null;
if (locatorIsASupportedPackageManager) {
url = spec.url.replace(`{}`, version);
if (process.env.COREPACK_NPM_REGISTRY) {
const registry = getRegistryFromPackageManagerSpec(spec);
if (registry.type === `npm`) {
url = await npmRegistryUtils.fetchTarballUrl(registry.package, version);
({tarball: url, signatures, integrity} = await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version));
if (registry.bin) {
binPath = registry.bin;
}
Expand All @@ -246,7 +248,7 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}

debugUtils.log(`Installing ${locator.name}@${version} from ${url}`);
const algo = build[0] ?? `sha256`;
const algo = build[0] ?? `sha512`;
const {tmpFolder, outputFile, hash: actualHash} = await download(installTarget, url, algo, binPath);

let bin: BinSpec | BinList;
Expand Down Expand Up @@ -279,6 +281,17 @@ export async function installVersion(installTarget: string, locator: Locator, {s
}
}

if (!build[1]) {
const registry = getRegistryFromPackageManagerSpec(spec);
if (registry.type === `npm` && !registry.bin && process.env.COREPACK_INTEGRITY_KEYS !== ``) {
if (signatures! == null || integrity! == null)
({signatures, integrity} = (await npmRegistryUtils.fetchTarballURLAndSignature(registry.package, version)));

npmRegistryUtils.verifySignature({signatures, integrity, packageName: registry.package, version});
// @ts-expect-error ignore readonly
build[1] = Buffer.from(integrity.slice(`sha512-`.length), `base64`).toString(`hex`);
}
}
if (build[1] && actualHash !== build[1])
throw new Error(`Mismatch hashes. Expected ${build[1]}, got ${actualHash}`);

Expand Down
47 changes: 28 additions & 19 deletions sources/httpUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,27 +16,25 @@ async function fetch(input: string | URL, init?: RequestInit) {
input = new URL(input);

let headers = init?.headers;
const {username, password} = input;

const username: string | undefined = input.username ?? process.env.COREPACK_NPM_USERNAME;
const password: string | undefined = input.password ?? process.env.COREPACK_NPM_PASSWORD;

if (username || password) {
headers = {
...headers,
authorization: `Bearer ${Buffer.from(`${username}:${password}`).toString(`base64`)}`,
authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(`base64`)}`,
};

input.username = input.password = ``;
} else if (input.origin === process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL) {
if (process.env.COREPACK_NPM_TOKEN) {
headers = {
...headers,
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
};
} else if (`COREPACK_NPM_PASSWORD` in process.env) {
headers = {
...headers,
authorization: `Bearer ${Buffer.from(`${process.env.COREPACK_NPM_USER}:${process.env.COREPACK_NPM_PASSWORD}`).toString(`base64`)}`,
};
}
}

if (input.origin === (process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL) && process.env.COREPACK_NPM_TOKEN) {
headers = {
...headers,
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
};
}

let response;
try {
Expand Down Expand Up @@ -92,6 +90,8 @@ export async function fetchUrlStream(input: string | URL, init?: RequestInit) {
return stream;
}

let ProxyAgent: typeof import('undici').ProxyAgent;

async function getProxyAgent(input: string | URL) {
const {getProxyForUrl} = await import(`proxy-from-env`);

Expand All @@ -100,11 +100,20 @@ async function getProxyAgent(input: string | URL) {

if (!proxy) return undefined;

// Doing a deep import here since undici isn't tree-shakeable
const {default: ProxyAgent} = (await import(
// @ts-expect-error No types for this specific file
`undici/lib/proxy-agent.js`
)) as { default: typeof import('undici').ProxyAgent };
if (ProxyAgent == null) {
// Doing a deep import here since undici isn't tree-shakeable
const [api, Dispatcher, _ProxyAgent] = await Promise.all([
// @ts-expect-error internal module is untyped
import(`undici/lib/api/index.js`),
// @ts-expect-error internal module is untyped
import(`undici/lib/dispatcher/dispatcher.js`),
// @ts-expect-error internal module is untyped
import(`undici/lib/dispatcher/proxy-agent.js`),
]);

Object.assign(Dispatcher.default.prototype, api.default);
ProxyAgent = _ProxyAgent.default;
}

return new ProxyAgent(proxy);
}
48 changes: 43 additions & 5 deletions sources/npmRegistryUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {UsageError} from 'clipanion';
import {createVerify} from 'crypto';

import defaultConfig from '../config.json';

import * as httpUtils from './httpUtils';

Expand Down Expand Up @@ -28,11 +31,46 @@ export async function fetchAsJson(packageName: string, version?: string) {
return httpUtils.fetchAsJson(`${npmRegistryUrl}/${packageName}${version ? `/${version}` : ``}`, {headers});
}

export function verifySignature({signatures, integrity, packageName, version}: {
signatures: Array<{keyid: string, sig: string}>;
integrity: string;
packageName: string;
version: string;
}) {
const {npm: keys} = process.env.COREPACK_INTEGRITY_KEYS ?
JSON.parse(process.env.COREPACK_INTEGRITY_KEYS) as typeof defaultConfig.keys :
defaultConfig.keys;

const key = keys.find(({keyid}) => signatures.some(s => s.keyid === keyid));
const signature = signatures.find(({keyid}) => keyid === key?.keyid);

if (key == null || signature == null) throw new Error(`Cannot find matching keyid: ${JSON.stringify({signatures, keys})}`);

const verifier = createVerify(`SHA256`);
verifier.end(`${packageName}@${version}:${integrity}`);
const valid = verifier.verify(
`-----BEGIN PUBLIC KEY-----\n${key.key}\n-----END PUBLIC KEY-----`,
signature.sig,
`base64`,
);
if (!valid) {
throw new Error(`Signature does not match`);
}
}

export async function fetchLatestStableVersion(packageName: string) {
const metadata = await fetchAsJson(packageName, `latest`);

const {shasum} = metadata.dist;
return `${metadata.version}+sha1.${shasum}`;
const {version, dist: {integrity, signatures}} = metadata;

if (process.env.COREPACK_INTEGRITY_KEYS !== ``) {
verifySignature({
packageName, version,
integrity, signatures,
});
}

return `${version}+sha512.${Buffer.from(integrity.slice(7), `base64`).toString(`hex`)}`;
}

export async function fetchAvailableTags(packageName: string) {
Expand All @@ -45,11 +83,11 @@ export async function fetchAvailableVersions(packageName: string) {
return Object.keys(metadata.versions);
}

export async function fetchTarballUrl(packageName: string, version: string) {
export async function fetchTarballURLAndSignature(packageName: string, version: string) {
const versionMetadata = await fetchAsJson(packageName, version);
const {tarball} = versionMetadata.dist;
const {tarball, signatures, integrity} = versionMetadata.dist;
if (tarball === undefined || !tarball.startsWith(`http`))
throw new Error(`${packageName}@${version} does not have a valid tarball.`);

return tarball;
return {tarball, signatures, integrity};
}
10 changes: 10 additions & 0 deletions sources/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ export interface Config {
};
};
};

keys: {
[registry: string]: Array<{
expires: null;
keyid: string;
keytype: string;
scheme: string;
key: string;
}>;
};
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/Up.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe(`UpCommand`, () => {
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `[email protected]+sha256.8c1575156cfa42112242cc5cfbbd1049da9448ffcdb5c55ce996883610ea983f`,
packageManager: `[email protected]+sha512.8dd9fedc5451829619e526c56f42609ad88ae4776d9d3f9456d578ac085115c0c2f0fb02bb7d57fd2e1b6e1ac96efba35e80a20a056668f61c96934f67694fd0`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
Expand Down
4 changes: 2 additions & 2 deletions tests/Use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe(`UseCommand`, () => {
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `[email protected]+sha256.bc5316aa110b2f564a71a3d6e235be55b98714660870c5b6b2d2d3f12587fb58`,
packageManager: `[email protected]+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
Expand All @@ -40,7 +40,7 @@ describe(`UseCommand`, () => {
});

await expect(xfs.readJsonPromise(ppath.join(cwd, `package.json`))).resolves.toMatchObject({
packageManager: `[email protected]+sha256.bc5316aa110b2f564a71a3d6e235be55b98714660870c5b6b2d2d3f12587fb58`,
packageManager: `[email protected]+sha512.a1833b862fe52169bd6c2a033045a07df5bc6a23595c259e675fed1b2d035ab37abe6ce309720abb6636d68f03615054b6292dc0a70da31c8697fda228b50d18`,
});

await expect(runCli(cwd, [`yarn`, `--version`])).resolves.toMatchObject({
Expand Down
Loading

0 comments on commit b069c20

Please sign in to comment.