Skip to content

Commit d267753

Browse files
authored
fix: re-add support for custom registries with auth (#397)
1 parent 082fabf commit d267753

File tree

5 files changed

+252
-8
lines changed

5 files changed

+252
-8
lines changed

sources/corepackUtils.ts

+6
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,12 @@ export async function installVersion(installTarget: string, locator: Locator, {s
174174
}
175175
} else {
176176
url = decodeURIComponent(version);
177+
if (process.env.COREPACK_NPM_REGISTRY && url.startsWith(npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL)) {
178+
url = url.replace(
179+
npmRegistryUtils.DEFAULT_NPM_REGISTRY_URL,
180+
() => process.env.COREPACK_NPM_REGISTRY!,
181+
);
182+
}
177183
}
178184

179185
// Creating a temporary folder inside the install folder means that we

sources/httpUtils.ts

+34-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
1-
import assert from 'assert';
2-
import {UsageError} from 'clipanion';
3-
import {once} from 'events';
4-
import {stderr, stdin} from 'process';
5-
import {Readable} from 'stream';
1+
import assert from 'assert';
2+
import {UsageError} from 'clipanion';
3+
import {once} from 'events';
4+
import {stderr, stdin} from 'process';
5+
import {Readable} from 'stream';
6+
7+
import {DEFAULT_NPM_REGISTRY_URL} from './npmRegistryUtils';
68

79
async function fetch(input: string | URL, init?: RequestInit) {
810
if (process.env.COREPACK_ENABLE_NETWORK === `0`)
911
throw new UsageError(`Network access disabled by the environment; can't reach ${input}`);
1012

1113
const agent = await getProxyAgent(input);
1214

15+
if (typeof input === `string`)
16+
input = new URL(input);
17+
18+
let headers = init?.headers;
19+
const {username, password} = input;
20+
if (username || password) {
21+
headers = {
22+
...headers,
23+
authorization: `Bearer ${Buffer.from(`${username}:${password}`).toString(`base64`)}`,
24+
};
25+
input.username = input.password = ``;
26+
} else if (input.origin === process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL) {
27+
if (process.env.COREPACK_NPM_TOKEN) {
28+
headers = {
29+
...headers,
30+
authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`,
31+
};
32+
} else if (`COREPACK_NPM_PASSWORD` in process.env) {
33+
headers = {
34+
...headers,
35+
authorization: `Bearer ${Buffer.from(`${process.env.COREPACK_NPM_USER}:${process.env.COREPACK_NPM_PASSWORD}`).toString(`base64`)}`,
36+
};
37+
}
38+
}
39+
40+
1341
let response;
1442
try {
1543
response = await globalThis.fetch(input, {
1644
...init,
1745
dispatcher: agent,
46+
headers,
1847
});
1948
} catch (error) {
2049
throw new Error(

tests/_registryServer.mjs

+155
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import {createHash} from 'node:crypto';
2+
import {once} from 'node:events';
3+
import {createServer} from 'node:http';
4+
import {gzipSync} from 'node:zlib';
5+
6+
function createSimpleTarArchive(fileName, fileContent, mode = 0o644) {
7+
const contentBuffer = Buffer.from(fileContent);
8+
9+
const header = Buffer.alloc(512); // TAR headers are 512 bytes
10+
header.write(fileName);
11+
header.write(`100${mode.toString(8)} `, 100, 7, `utf-8`); // File mode (octal) followed by a space
12+
header.write(`0001750 `, 108, 8, `utf-8`); // Owner's numeric user ID (octal) followed by a space
13+
header.write(`0001750 `, 116, 8, `utf-8`); // Group's numeric user ID (octal) followed by a space
14+
header.write(`${contentBuffer.length.toString(8)} `, 124, 12, `utf-8`); // File size in bytes (octal) followed by a space
15+
header.write(`${Math.floor(Date.now() / 1000).toString(8)} `, 136, 12, `utf-8`); // Last modification time in numeric Unix time format (octal) followed by a space
16+
header.fill(` `, 148, 156); // Fill checksum area with spaces for calculation
17+
header.write(`ustar `, 257, 8, `utf-8`); // UStar indicator
18+
19+
// Calculate and write the checksum. Note: This is a simplified calculation not recommended for production
20+
const checksum = header.reduce((sum, value) => sum + value, 0);
21+
header.write(`${checksum.toString(8)}\0 `, 148, 8, `utf-8`); // Write checksum in octal followed by null and space
22+
23+
24+
return Buffer.concat([
25+
header,
26+
contentBuffer,
27+
Buffer.alloc(512 - (contentBuffer.length % 512)),
28+
]);
29+
}
30+
31+
const mockPackageTarGz = gzipSync(Buffer.concat([
32+
createSimpleTarArchive(`package/bin/customPkgManager.js`, `#!/usr/bin/env node\nconsole.log("customPkgManager: Hello from custom registry");\n`, 0o755),
33+
createSimpleTarArchive(`package/bin/pnpm.js`, `#!/usr/bin/env node\nconsole.log("pnpm: Hello from custom registry");\n`, 0o755),
34+
createSimpleTarArchive(`package/bin/yarn.js`, `#!/usr/bin/env node\nconsole.log("yarn: Hello from custom registry");\n`, 0o755),
35+
createSimpleTarArchive(`package/package.json`, JSON.stringify({bin: {yarn: `bin/yarn.js`, pnpm: `bin/pnpm.js`, customPkgManager: `bin/customPkgManager.js`}})),
36+
Buffer.alloc(1024),
37+
]));
38+
const shasum = createHash(`sha1`).update(mockPackageTarGz).digest(`hex`);
39+
40+
41+
const server = createServer((req, res) => {
42+
const auth = req.headers.authorization;
43+
if (!auth?.startsWith(`Bearer `) || Buffer.from(auth.slice(`Bearer `.length), `base64`).toString() !== `user:pass`) {
44+
res.statusCode = 401;
45+
res.end(`Unauthorized`);
46+
return;
47+
}
48+
switch (req.url) {
49+
case `/yarn`: {
50+
res.end(JSON.stringify({"dist-tags": {
51+
latest: `1.9998.9999`,
52+
}, versions: {'1.9998.9999': {
53+
dist: {
54+
shasum,
55+
size: mockPackageTarGz.length,
56+
noattachment: false,
57+
tarball: `${process.env.COREPACK_NPM_REGISTRY}/yarn.tgz`,
58+
},
59+
}}}));
60+
break;
61+
}
62+
63+
case `/pnpm`: {
64+
res.end(JSON.stringify({"dist-tags": {
65+
latest: `1.9998.9999`,
66+
}, versions: {'1.9998.9999': {
67+
dist: {
68+
shasum,
69+
size: mockPackageTarGz.length,
70+
noattachment: false,
71+
tarball: `${process.env.COREPACK_NPM_REGISTRY}/pnpm/-/pnpm-1.9998.9999.tgz`,
72+
},
73+
}}}));
74+
break;
75+
}
76+
77+
case `/@yarnpkg/cli-dist`: {
78+
res.end(JSON.stringify({"dist-tags": {
79+
latest: `5.9999.9999`,
80+
}, versions: {'5.9999.9999': {
81+
bin: {
82+
yarn: `./bin/yarn.js`,
83+
yarnpkg: `./bin/yarn.js`,
84+
},
85+
dist: {
86+
shasum,
87+
size: mockPackageTarGz.length,
88+
noattachment: false,
89+
tarball: `${process.env.COREPACK_NPM_REGISTRY}/yarn.tgz`,
90+
},
91+
}}}));
92+
break;
93+
}
94+
95+
case `/customPkgManager`: {
96+
res.end(JSON.stringify({"dist-tags": {
97+
latest: `1.0.0`,
98+
}, versions: {'1.0.0': {
99+
bin: {
100+
customPkgManager: `./bin/customPkgManager.js`,
101+
},
102+
dist: {
103+
shasum,
104+
size: mockPackageTarGz.length,
105+
noattachment: false,
106+
tarball: `${process.env.COREPACK_NPM_REGISTRY}/customPkgManager/-/customPkgManager-1.0.0.tgz`,
107+
},
108+
}}}));
109+
break;
110+
}
111+
112+
case `/pnpm/-/pnpm-1.9998.9999.tgz`:
113+
case `/yarn.tgz`:
114+
case `/customPkgManager/-/customPkgManager-1.0.0.tgz`:
115+
res.end(mockPackageTarGz);
116+
break;
117+
118+
default:
119+
throw new Error(`unsupported request`, {cause: req.url});
120+
}
121+
}).listen(0, `localhost`);
122+
123+
await once(server, `listening`);
124+
125+
const {address, port} = server.address();
126+
switch (process.env.AUTH_TYPE) {
127+
case `COREPACK_NPM_REGISTRY`:
128+
process.env.COREPACK_NPM_REGISTRY = `http://user:pass@${address.includes(`:`) ? `[${address}]` : address}:${port}`;
129+
break;
130+
131+
case `COREPACK_NPM_TOKEN`:
132+
process.env.COREPACK_NPM_REGISTRY = `http://${address.includes(`:`) ? `[${address}]` : address}:${port}`;
133+
process.env.COREPACK_NPM_TOKEN = Buffer.from(`user:pass`).toString(`base64`);
134+
break;
135+
136+
case `COREPACK_NPM_PASSWORD`:
137+
process.env.COREPACK_NPM_REGISTRY = `http://${address.includes(`:`) ? `[${address}]` : address}:${port}`;
138+
process.env.COREPACK_NPM_USER = `user`;
139+
process.env.COREPACK_NPM_PASSWORD = `pass`;
140+
break;
141+
142+
default: throw new Error(`Invalid AUTH_TYPE in env`, {cause: process.env.AUTH_TYPE});
143+
}
144+
145+
if (process.env.NOCK_ENV === `replay`) {
146+
const originalFetch = globalThis.fetch;
147+
globalThis.fetch = function fetch(i) {
148+
if (!`${i}`.startsWith(`http://${address.includes(`:`) ? `[${address}]` : address}:${port}`))
149+
throw new Error;
150+
151+
return Reflect.apply(originalFetch, this, arguments);
152+
};
153+
}
154+
155+
server.unref();

tests/_runCli.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import {PortablePath, npath} from '@yarnpkg/fslib';
22
import {spawn} from 'child_process';
3+
import * as path from 'path';
4+
import {pathToFileURL} from 'url';
35

4-
export async function runCli(cwd: PortablePath, argv: Array<string>): Promise<{exitCode: number | null, stdout: string, stderr: string}> {
6+
export async function runCli(cwd: PortablePath, argv: Array<string>, withCustomRegistry?: boolean): Promise<{exitCode: number | null, stdout: string, stderr: string}> {
57
const out: Array<Buffer> = [];
68
const err: Array<Buffer> = [];
79

810
return new Promise((resolve, reject) => {
9-
const child = spawn(process.execPath, [`--no-warnings`, `-r`, require.resolve(`./recordRequests.js`), require.resolve(`../dist/corepack.js`), ...argv], {
11+
const child = spawn(process.execPath, [`--no-warnings`, ...(withCustomRegistry ? [`--import`, pathToFileURL(path.join(__dirname, `_registryServer.mjs`)) as any as string] : [`-r`, require.resolve(`./recordRequests.js`)]), require.resolve(`../dist/corepack.js`), ...argv], {
1012
cwd: npath.fromPortablePath(cwd),
1113
env: process.env,
1214
stdio: `pipe`,

tests/main.test.ts

+53-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {beforeEach, it, expect} from '@jest/globals';
1+
import {beforeEach, describe, expect, it} from '@jest/globals';
22
import {Filename, ppath, xfs, npath, PortablePath} from '@yarnpkg/fslib';
33
import process from 'node:process';
44

@@ -804,3 +804,55 @@ it(`should download yarn berry from custom registry`, async () => {
804804
});
805805
});
806806
});
807+
808+
for (const authType of [`COREPACK_NPM_REGISTRY`, `COREPACK_NPM_TOKEN`, `COREPACK_NPM_PASSWORD`]) {
809+
describe(`custom registry with auth ${authType}`, () => {
810+
beforeEach(() => {
811+
process.env.AUTH_TYPE = authType; // See `_registryServer.mjs`
812+
});
813+
814+
it(`should download yarn classic`, async () => {
815+
await xfs.mktempPromise(async cwd => {
816+
await expect(runCli(cwd, [`yarn@1.x`, `--version`], true)).resolves.toMatchObject({
817+
exitCode: 0,
818+
stdout: `yarn: Hello from custom registry\n`,
819+
stderr: ``,
820+
});
821+
});
822+
});
823+
824+
it(`should download yarn berry`, async () => {
825+
await xfs.mktempPromise(async cwd => {
826+
await xfs.writeJsonPromise(ppath.join(cwd, `package.json` as Filename), {
827+
packageManager: `yarn@3.0.0`,
828+
});
829+
830+
await expect(runCli(cwd, [`yarn@5.x`, `--version`], true)).resolves.toMatchObject({
831+
exitCode: 0,
832+
stdout: `yarn: Hello from custom registry\n`,
833+
stderr: ``,
834+
});
835+
});
836+
});
837+
838+
it(`should download pnpm`, async () => {
839+
await xfs.mktempPromise(async cwd => {
840+
await expect(runCli(cwd, [`pnpm@1.x`, `--version`], true)).resolves.toMatchObject({
841+
exitCode: 0,
842+
stdout: `pnpm: Hello from custom registry\n`,
843+
stderr: ``,
844+
});
845+
});
846+
});
847+
848+
it(`should download custom package manager`, async () => {
849+
await xfs.mktempPromise(async cwd => {
850+
await expect(runCli(cwd, [`customPkgManager@https://registry.npmjs.org/customPkgManager/-/customPkgManager-1.0.0.tgz`, `--version`], true)).resolves.toMatchObject({
851+
exitCode: 0,
852+
stdout: `customPkgManager: Hello from custom registry\n`,
853+
stderr: ``,
854+
});
855+
});
856+
});
857+
});
858+
}

0 commit comments

Comments
 (0)