Skip to content

Commit

Permalink
feat: add support for configurable registries and applicable auth opt…
Browse files Browse the repository at this point in the history
…ions (#186)

Fixes: #66
  • Loading branch information
micsco authored Oct 28, 2022
1 parent 0ec3a73 commit 662ae90
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 7 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,12 @@ This command will retrieve the given package manager from the specified archive

- `COREPACK_ROOT` has no functional impact on Corepack itself; it's automatically being set in your environment by Corepack when it shells out to the underlying package managers, so that they can feature-detect its presence (useful for commands like `yarn init`).

- `COREPACK_NPM_REGISTRY` sets the registry base url used when retrieving package managers from npm. Default value is `https://registry.npmjs.org`

- `COREPACK_NPM_TOKEN` sets a Bearer token authorization header when connecting to a npm type registry.

- `COREPACK_NPM_USERNAME` and `COREPACK_NPM_PASSWORD` to set a Basic authorization header when connecting to a npm type registry. Note that both environment variables are required and as plain text. If you want to send an empty password, explicitly set `COREPACK_NPM_PASSWORD` to an empty string.

- `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through [`node-proxy-agent`](https://github.com/TooTallNate/node-proxy-agent).

## Contributing
Expand Down
11 changes: 4 additions & 7 deletions sources/corepackUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import * as folderUtils from './folderUti
import * as fsUtils from './fsUtils';
import * as httpUtils from './httpUtils';
import * as nodeUtils from './nodeUtils';
import * as npmRegistryUtils from './npmRegistryUtils';
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';

export async function fetchLatestStableVersion(spec: RegistrySpec) {
switch (spec.type) {
case `npm`: {
const {[`dist-tags`]: {latest}, versions: {[latest]: {dist: {shasum}}}} =
await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`);
return `${latest}+sha1.${shasum}`;
return await npmRegistryUtils.fetchLatestStableVersion(spec.package);
}
case `url`: {
const data = await httpUtils.fetchAsJson(spec.url);
Expand All @@ -32,8 +31,7 @@ export async function fetchLatestStableVersion(spec: RegistrySpec) {
export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<string, string>> {
switch (spec.type) {
case `npm`: {
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
return data[`dist-tags`];
return await npmRegistryUtils.fetchAvailableTags(spec.package);
}
case `url`: {
const data = await httpUtils.fetchAsJson(spec.url);
Expand All @@ -48,8 +46,7 @@ export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<str
export async function fetchAvailableVersions(spec: RegistrySpec): Promise<Array<string>> {
switch (spec.type) {
case `npm`: {
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
return Object.keys(data.versions);
return await npmRegistryUtils.fetchAvailableVersions(spec.package);
}
case `url`: {
const data = await httpUtils.fetchAsJson(spec.url);
Expand Down
50 changes: 50 additions & 0 deletions sources/npmRegistryUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {UsageError} from 'clipanion';
import {OutgoingHttpHeaders} from 'http2';

import * as httpUtils from './httpUtils';

// load abbreviated metadata as that's all we need for these calls
// see: https://github.com/npm/registry/blob/cfe04736f34db9274a780184d1cdb2fb3e4ead2a/docs/responses/package-metadata.md
export const DEFAULT_HEADERS: OutgoingHttpHeaders = {
[`Accept`]: `application/vnd.npm.install-v1+json`,
};
export const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org`;

export async function fetchAsJson(packageName: string) {
const npmRegistryUrl = process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL;

if (process.env.COREPACK_ENABLE_NETWORK === `0`)
throw new UsageError(`Network access disabled by the environment; can't reach npm repository ${npmRegistryUrl}`);

const headers = {...DEFAULT_HEADERS};

if (`COREPACK_NPM_TOKEN` in process.env) {
headers.authorization = `Bearer ${process.env.COREPACK_NPM_TOKEN}`;
} else if (`COREPACK_NPM_USERNAME` in process.env
&& `COREPACK_NPM_PASSWORD` in process.env) {
const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`);
headers.authorization = `Basic ${encodedCreds}`;
}

return httpUtils.fetchAsJson(`${npmRegistryUrl}/${packageName}`, {headers});
}

export async function fetchLatestStableVersion(packageName: string) {
const metadata = await fetchAsJson(packageName);
const {latest} = metadata[`dist-tags`];
if (latest === undefined) throw new Error(`${packageName} does not have a "latest" tag.`);

const {shasum} = metadata.versions[latest].dist;

return `${latest}+sha1.${shasum}`;
}

export async function fetchAvailableTags(packageName: string) {
const metadata = await fetchAsJson(packageName);
return metadata[`dist-tags`];
}

export async function fetchAvailableVersions(packageName: string) {
const metadata = await fetchAsJson(packageName);
return Object.keys(metadata.versions);
}
89 changes: 89 additions & 0 deletions tests/npmRegistryUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {fetchAsJson as httpFetchAsJson} from '../sources/httpUtils';
import {DEFAULT_HEADERS, DEFAULT_NPM_REGISTRY_URL, fetchAsJson} from '../sources/npmRegistryUtils';

jest.mock(`../sources/httpUtils`);

describe(`npm registry utils fetchAsJson`, () => {
const OLD_ENV = process.env;

beforeEach(() => {
process.env = {...OLD_ENV}; // Make a copy
jest.resetAllMocks();
});

afterEach(() => {
process.env = OLD_ENV; // Restore old environment
});

it(`throw usage error if COREPACK_ENABLE_NETWORK env is set to 0`, async () => {
process.env.COREPACK_ENABLE_NETWORK = `0`;

await expect(fetchAsJson(`package-name`)).rejects.toThrowError();
});

it(`loads from DEFAULT_NPM_REGISTRY_URL by default`, async () => {
await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: DEFAULT_HEADERS});
});

it(`loads from custom COREPACK_NPM_REGISTRY if set`, async () => {
process.env.COREPACK_NPM_REGISTRY = `https://registry.example.org`;
await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${process.env.COREPACK_NPM_REGISTRY}/package-name`, {headers: DEFAULT_HEADERS});
});

it(`adds authorization header with bearer token if COREPACK_NPM_TOKEN is set`, async () => {
process.env.COREPACK_NPM_TOKEN = `foo`;

await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
...DEFAULT_HEADERS,
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
}});
});

it(`only adds authorization header with bearer token if COREPACK_NPM_TOKEN and COREPACK_NPM_USERNAME are set`, async () => {
process.env.COREPACK_NPM_TOKEN = `foo`;
process.env.COREPACK_NPM_USERNAME = `bar`;
process.env.COREPACK_NPM_PASSWORD = `foobar`;

await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
...DEFAULT_HEADERS,
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
}});
});


it(`adds authorization header with basic auth if COREPACK_NPM_USERNAME and COREPACK_NPM_PASSWORD are set`, async () => {
process.env.COREPACK_NPM_USERNAME = `foo`;
process.env.COREPACK_NPM_PASSWORD = `bar`;

const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`);

await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
...DEFAULT_HEADERS,
authorization: `Basic ${encodedCreds}`,
}});
});

it(`does not add authorization header if COREPACK_NPM_USERNAME is set and COREPACK_NPM_PASSWORD is not.`, async () => {
process.env.COREPACK_NPM_USERNAME = `foo`;

await fetchAsJson(`package-name`);

expect(httpFetchAsJson).toBeCalled();
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: DEFAULT_HEADERS});
});
});

0 comments on commit 662ae90

Please sign in to comment.