Skip to content

Commit 662ae90

Browse files
authored
feat: add support for configurable registries and applicable auth options (#186)
Fixes: #66
1 parent 0ec3a73 commit 662ae90

File tree

4 files changed

+149
-7
lines changed

4 files changed

+149
-7
lines changed

README.md

+6
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,12 @@ This command will retrieve the given package manager from the specified archive
152152

153153
- `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`).
154154

155+
- `COREPACK_NPM_REGISTRY` sets the registry base url used when retrieving package managers from npm. Default value is `https://registry.npmjs.org`
156+
157+
- `COREPACK_NPM_TOKEN` sets a Bearer token authorization header when connecting to a npm type registry.
158+
159+
- `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.
160+
155161
- `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are supported through [`node-proxy-agent`](https://github.com/TooTallNate/node-proxy-agent).
156162

157163
## Contributing

sources/corepackUtils.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@ import * as folderUtils from './folderUti
1010
import * as fsUtils from './fsUtils';
1111
import * as httpUtils from './httpUtils';
1212
import * as nodeUtils from './nodeUtils';
13+
import * as npmRegistryUtils from './npmRegistryUtils';
1314
import {RegistrySpec, Descriptor, Locator, PackageManagerSpec} from './types';
1415

1516
export async function fetchLatestStableVersion(spec: RegistrySpec) {
1617
switch (spec.type) {
1718
case `npm`: {
18-
const {[`dist-tags`]: {latest}, versions: {[latest]: {dist: {shasum}}}} =
19-
await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`);
20-
return `${latest}+sha1.${shasum}`;
19+
return await npmRegistryUtils.fetchLatestStableVersion(spec.package);
2120
}
2221
case `url`: {
2322
const data = await httpUtils.fetchAsJson(spec.url);
@@ -32,8 +31,7 @@ export async function fetchLatestStableVersion(spec: RegistrySpec) {
3231
export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<string, string>> {
3332
switch (spec.type) {
3433
case `npm`: {
35-
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
36-
return data[`dist-tags`];
34+
return await npmRegistryUtils.fetchAvailableTags(spec.package);
3735
}
3836
case `url`: {
3937
const data = await httpUtils.fetchAsJson(spec.url);
@@ -48,8 +46,7 @@ export async function fetchAvailableTags(spec: RegistrySpec): Promise<Record<str
4846
export async function fetchAvailableVersions(spec: RegistrySpec): Promise<Array<string>> {
4947
switch (spec.type) {
5048
case `npm`: {
51-
const data = await httpUtils.fetchAsJson(`https://registry.npmjs.org/${spec.package}`, {headers: {[`Accept`]: `application/vnd.npm.install-v1+json`}});
52-
return Object.keys(data.versions);
49+
return await npmRegistryUtils.fetchAvailableVersions(spec.package);
5350
}
5451
case `url`: {
5552
const data = await httpUtils.fetchAsJson(spec.url);

sources/npmRegistryUtils.ts

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import {UsageError} from 'clipanion';
2+
import {OutgoingHttpHeaders} from 'http2';
3+
4+
import * as httpUtils from './httpUtils';
5+
6+
// load abbreviated metadata as that's all we need for these calls
7+
// see: https://github.com/npm/registry/blob/cfe04736f34db9274a780184d1cdb2fb3e4ead2a/docs/responses/package-metadata.md
8+
export const DEFAULT_HEADERS: OutgoingHttpHeaders = {
9+
[`Accept`]: `application/vnd.npm.install-v1+json`,
10+
};
11+
export const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org`;
12+
13+
export async function fetchAsJson(packageName: string) {
14+
const npmRegistryUrl = process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL;
15+
16+
if (process.env.COREPACK_ENABLE_NETWORK === `0`)
17+
throw new UsageError(`Network access disabled by the environment; can't reach npm repository ${npmRegistryUrl}`);
18+
19+
const headers = {...DEFAULT_HEADERS};
20+
21+
if (`COREPACK_NPM_TOKEN` in process.env) {
22+
headers.authorization = `Bearer ${process.env.COREPACK_NPM_TOKEN}`;
23+
} else if (`COREPACK_NPM_USERNAME` in process.env
24+
&& `COREPACK_NPM_PASSWORD` in process.env) {
25+
const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`);
26+
headers.authorization = `Basic ${encodedCreds}`;
27+
}
28+
29+
return httpUtils.fetchAsJson(`${npmRegistryUrl}/${packageName}`, {headers});
30+
}
31+
32+
export async function fetchLatestStableVersion(packageName: string) {
33+
const metadata = await fetchAsJson(packageName);
34+
const {latest} = metadata[`dist-tags`];
35+
if (latest === undefined) throw new Error(`${packageName} does not have a "latest" tag.`);
36+
37+
const {shasum} = metadata.versions[latest].dist;
38+
39+
return `${latest}+sha1.${shasum}`;
40+
}
41+
42+
export async function fetchAvailableTags(packageName: string) {
43+
const metadata = await fetchAsJson(packageName);
44+
return metadata[`dist-tags`];
45+
}
46+
47+
export async function fetchAvailableVersions(packageName: string) {
48+
const metadata = await fetchAsJson(packageName);
49+
return Object.keys(metadata.versions);
50+
}

tests/npmRegistryUtils.test.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import {fetchAsJson as httpFetchAsJson} from '../sources/httpUtils';
2+
import {DEFAULT_HEADERS, DEFAULT_NPM_REGISTRY_URL, fetchAsJson} from '../sources/npmRegistryUtils';
3+
4+
jest.mock(`../sources/httpUtils`);
5+
6+
describe(`npm registry utils fetchAsJson`, () => {
7+
const OLD_ENV = process.env;
8+
9+
beforeEach(() => {
10+
process.env = {...OLD_ENV}; // Make a copy
11+
jest.resetAllMocks();
12+
});
13+
14+
afterEach(() => {
15+
process.env = OLD_ENV; // Restore old environment
16+
});
17+
18+
it(`throw usage error if COREPACK_ENABLE_NETWORK env is set to 0`, async () => {
19+
process.env.COREPACK_ENABLE_NETWORK = `0`;
20+
21+
await expect(fetchAsJson(`package-name`)).rejects.toThrowError();
22+
});
23+
24+
it(`loads from DEFAULT_NPM_REGISTRY_URL by default`, async () => {
25+
await fetchAsJson(`package-name`);
26+
27+
expect(httpFetchAsJson).toBeCalled();
28+
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: DEFAULT_HEADERS});
29+
});
30+
31+
it(`loads from custom COREPACK_NPM_REGISTRY if set`, async () => {
32+
process.env.COREPACK_NPM_REGISTRY = `https://registry.example.org`;
33+
await fetchAsJson(`package-name`);
34+
35+
expect(httpFetchAsJson).toBeCalled();
36+
expect(httpFetchAsJson).lastCalledWith(`${process.env.COREPACK_NPM_REGISTRY}/package-name`, {headers: DEFAULT_HEADERS});
37+
});
38+
39+
it(`adds authorization header with bearer token if COREPACK_NPM_TOKEN is set`, async () => {
40+
process.env.COREPACK_NPM_TOKEN = `foo`;
41+
42+
await fetchAsJson(`package-name`);
43+
44+
expect(httpFetchAsJson).toBeCalled();
45+
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
46+
...DEFAULT_HEADERS,
47+
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
48+
}});
49+
});
50+
51+
it(`only adds authorization header with bearer token if COREPACK_NPM_TOKEN and COREPACK_NPM_USERNAME are set`, async () => {
52+
process.env.COREPACK_NPM_TOKEN = `foo`;
53+
process.env.COREPACK_NPM_USERNAME = `bar`;
54+
process.env.COREPACK_NPM_PASSWORD = `foobar`;
55+
56+
await fetchAsJson(`package-name`);
57+
58+
expect(httpFetchAsJson).toBeCalled();
59+
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
60+
...DEFAULT_HEADERS,
61+
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
62+
}});
63+
});
64+
65+
66+
it(`adds authorization header with basic auth if COREPACK_NPM_USERNAME and COREPACK_NPM_PASSWORD are set`, async () => {
67+
process.env.COREPACK_NPM_USERNAME = `foo`;
68+
process.env.COREPACK_NPM_PASSWORD = `bar`;
69+
70+
const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`);
71+
72+
await fetchAsJson(`package-name`);
73+
74+
expect(httpFetchAsJson).toBeCalled();
75+
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: {
76+
...DEFAULT_HEADERS,
77+
authorization: `Basic ${encodedCreds}`,
78+
}});
79+
});
80+
81+
it(`does not add authorization header if COREPACK_NPM_USERNAME is set and COREPACK_NPM_PASSWORD is not.`, async () => {
82+
process.env.COREPACK_NPM_USERNAME = `foo`;
83+
84+
await fetchAsJson(`package-name`);
85+
86+
expect(httpFetchAsJson).toBeCalled();
87+
expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: DEFAULT_HEADERS});
88+
});
89+
});

0 commit comments

Comments
 (0)