From 54a8fb95ad7f90b98e425ee936f0a74b8b9a8329 Mon Sep 17 00:00:00 2001 From: Bob <25150818+BobLuursema@users.noreply.github.com> Date: Wed, 17 Sep 2025 12:00:41 +0200 Subject: [PATCH 1/5] feat: Allow token to be sent on download --- sources/httpUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sources/httpUtils.ts b/sources/httpUtils.ts index 6c2dc9040..4da683320 100644 --- a/sources/httpUtils.ts +++ b/sources/httpUtils.ts @@ -29,7 +29,7 @@ async function fetch(input: string | URL, init?: RequestInit) { input.username = input.password = ``; } - if (input.origin === (process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL) && process.env.COREPACK_NPM_TOKEN) { + if ((process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL).includes(input.origin) && process.env.COREPACK_NPM_TOKEN) { headers = { ...headers, authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, From 4c35677276e90b770367bcb6f43f1130877b8061 Mon Sep 17 00:00:00 2001 From: Bob <25150818+BobLuursema@users.noreply.github.com> Date: Thu, 18 Sep 2025 15:32:56 +0200 Subject: [PATCH 2/5] Refactor authentication code and add tests --- sources/httpUtils.ts | 11 +++-- sources/npmRegistryUtils.ts | 8 ---- tests/npmRegistryUtils.test.ts | 87 +++++++++++++++++++++++++--------- 3 files changed, 71 insertions(+), 35 deletions(-) diff --git a/sources/httpUtils.ts b/sources/httpUtils.ts index 4da683320..e04545834 100644 --- a/sources/httpUtils.ts +++ b/sources/httpUtils.ts @@ -20,17 +20,18 @@ async function fetch(input: string | URL, init?: RequestInit) { 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 = { + if (username && password) { + const encodedCreds = Buffer.from(`${username}:${password}`, `utf8`).toString(`base64`); + headers = { ...headers, - authorization: `Basic ${Buffer.from(`${username}:${password}`).toString(`base64`)}`, + authorization: `Basic ${encodedCreds}`, }; input.username = input.password = ``; } - if ((process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL).includes(input.origin) && process.env.COREPACK_NPM_TOKEN) { - headers = { + if (input.origin === new URL(process.env.COREPACK_NPM_REGISTRY || DEFAULT_NPM_REGISTRY_URL).origin && process.env.COREPACK_NPM_TOKEN) { + headers = { ...headers, authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, }; diff --git a/sources/npmRegistryUtils.ts b/sources/npmRegistryUtils.ts index a7d110e95..46b9a8328 100644 --- a/sources/npmRegistryUtils.ts +++ b/sources/npmRegistryUtils.ts @@ -21,14 +21,6 @@ export async function fetchAsJson(packageName: string, version?: string) { 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}${version ? `/${version}` : ``}`, {headers}); } diff --git a/tests/npmRegistryUtils.test.ts b/tests/npmRegistryUtils.test.ts index 728d6f977..545746be7 100644 --- a/tests/npmRegistryUtils.test.ts +++ b/tests/npmRegistryUtils.test.ts @@ -5,7 +5,11 @@ import {describe, beforeEach, it, expect, vi} from 'vitest'; import {fetchAsJson as httpFetchAsJson} from '../sources/httpUtils'; import {DEFAULT_HEADERS, DEFAULT_NPM_REGISTRY_URL, fetchAsJson} from '../sources/npmRegistryUtils'; -vi.mock(`../sources/httpUtils`); +const fetchMock = vi.fn(() => Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), +})); +vi.stubGlobal(`fetch`, fetchMock); describe(`npm registry utils fetchAsJson`, () => { beforeEach(() => { @@ -22,8 +26,8 @@ describe(`npm registry utils fetchAsJson`, () => { 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}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({headers: DEFAULT_HEADERS})); }); it(`loads from custom COREPACK_NPM_REGISTRY if set`, async () => { @@ -31,8 +35,8 @@ describe(`npm registry utils fetchAsJson`, () => { 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}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${process.env.COREPACK_NPM_REGISTRY}/package-name`), expect.objectContaining({headers: DEFAULT_HEADERS})); }); it(`adds authorization header with bearer token if COREPACK_NPM_TOKEN is set`, async () => { @@ -41,11 +45,13 @@ describe(`npm registry utils fetchAsJson`, () => { 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}`, - }}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ + 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 () => { @@ -56,11 +62,13 @@ describe(`npm registry utils fetchAsJson`, () => { 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}`, - }}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ + headers: { + ...DEFAULT_HEADERS, + authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, + }, + })); }); @@ -73,11 +81,13 @@ describe(`npm registry utils fetchAsJson`, () => { await fetchAsJson(`package-name`); - expect(httpFetchAsJson).toBeCalled(); - expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: { - ...DEFAULT_HEADERS, - authorization: `Basic ${encodedCreds}`, - }}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ + 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 () => { @@ -86,7 +96,40 @@ describe(`npm registry utils fetchAsJson`, () => { await fetchAsJson(`package-name`); - expect(httpFetchAsJson).toBeCalled(); - expect(httpFetchAsJson).lastCalledWith(`${DEFAULT_NPM_REGISTRY_URL}/package-name`, {headers: DEFAULT_HEADERS}); + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({headers: DEFAULT_HEADERS})); + }); + + it(`does add authorization header if registry url contains a path`, async () => { + process.env.COREPACK_NPM_REGISTRY = `https://registry.example.org/some/path`; + process.env.COREPACK_NPM_TOKEN = `foo`; + + await fetchAsJson(`package-name`); + + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`https://registry.example.org/some/path/package-name`), expect.objectContaining({ + headers: { + ...DEFAULT_HEADERS, + authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, + }, + })); + }); +}); + +describe(`httpUtils fetchAsJson`, () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it(`does not add authorization header if the origin is different from the registry origin`, async () => { + process.env.COREPACK_NPM_REGISTRY = `https://registry.example.org/some/path`; + process.env.COREPACK_NPM_TOKEN = `foo`; + + await httpFetchAsJson(`https://another-registry.example.org/package-name`); + + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`https://another-registry.example.org/package-name`), expect.objectContaining({ + headers: undefined, + })); }); }); From d74900622a3f8eae0b169f4aaa48acf7b565a961 Mon Sep 17 00:00:00 2001 From: Bob <25150818+BobLuursema@users.noreply.github.com> Date: Sun, 21 Sep 2025 09:27:25 +0200 Subject: [PATCH 3/5] Make test expectation explicit --- tests/npmRegistryUtils.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/npmRegistryUtils.test.ts b/tests/npmRegistryUtils.test.ts index 545746be7..b4201053a 100644 --- a/tests/npmRegistryUtils.test.ts +++ b/tests/npmRegistryUtils.test.ts @@ -66,7 +66,7 @@ describe(`npm registry utils fetchAsJson`, () => { expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ headers: { ...DEFAULT_HEADERS, - authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, + authorization: `Bearer foo`, }, })); }); @@ -110,7 +110,7 @@ describe(`npm registry utils fetchAsJson`, () => { expect(fetchMock).lastCalledWith(new URL(`https://registry.example.org/some/path/package-name`), expect.objectContaining({ headers: { ...DEFAULT_HEADERS, - authorization: `Bearer ${process.env.COREPACK_NPM_TOKEN}`, + authorization: `Bearer foo`, }, })); }); From 3603766e203998091fbbf0b4281744caf284accf Mon Sep 17 00:00:00 2001 From: Bob <25150818+BobLuursema@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:36:03 +0200 Subject: [PATCH 4/5] Fix username/password auth --- sources/httpUtils.ts | 4 ++-- tests/npmRegistryUtils.test.ts | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/sources/httpUtils.ts b/sources/httpUtils.ts index e04545834..3b3f1419c 100644 --- a/sources/httpUtils.ts +++ b/sources/httpUtils.ts @@ -20,8 +20,8 @@ async function fetch(input: string | URL, init?: RequestInit) { 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) { - const encodedCreds = Buffer.from(`${username}:${password}`, `utf8`).toString(`base64`); + if (username || password) { + const encodedCreds = Buffer.from(`${username ?? ''}:${password ?? ''}`, `utf8`).toString(`base64`); headers = { ...headers, authorization: `Basic ${encodedCreds}`, diff --git a/tests/npmRegistryUtils.test.ts b/tests/npmRegistryUtils.test.ts index b4201053a..eb21b8aa5 100644 --- a/tests/npmRegistryUtils.test.ts +++ b/tests/npmRegistryUtils.test.ts @@ -90,14 +90,36 @@ describe(`npm registry utils fetchAsJson`, () => { })); }); - it(`does not add authorization header if COREPACK_NPM_USERNAME is set and COREPACK_NPM_PASSWORD is not.`, async () => { + it(`adds authorization header if COREPACK_NPM_USERNAME is set and COREPACK_NPM_PASSWORD is not.`, async () => { // `process.env` is reset after each tests in setupTests.js. process.env.COREPACK_NPM_USERNAME = `foo`; + const encodedCreds = Buffer.from(`${process.env.COREPACK_NPM_USERNAME}:`, `utf8`).toString(`base64`); + await fetchAsJson(`package-name`); expect(fetchMock).toBeCalled(); - expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({headers: DEFAULT_HEADERS})); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ + headers: { + ...DEFAULT_HEADERS, + authorization: `Basic ${encodedCreds}` + })); + }); + + it(`adds authorization header if COREPACK_NPM_PASSWORD is set and COREPACK_NPM_USERNAME is not.`, async () => { + // `process.env` is reset after each tests in setupTests.js. + process.env.COREPACK_NPM_PASSWORD = `foo`; + + const encodedCreds = Buffer.from(`:${process.env.COREPACK_NPM_PASSWORD}`, `utf8`).toString(`base64`); + + await fetchAsJson(`package-name`); + + expect(fetchMock).toBeCalled(); + expect(fetchMock).lastCalledWith(new URL(`${DEFAULT_NPM_REGISTRY_URL}/package-name`), expect.objectContaining({ + headers: { + ...DEFAULT_HEADERS, + authorization: `Basic ${encodedCreds}` + })); }); it(`does add authorization header if registry url contains a path`, async () => { From e4b8daaf4a393b793aab77773c333c0f17412d61 Mon Sep 17 00:00:00 2001 From: Bob <25150818+BobLuursema@users.noreply.github.com> Date: Wed, 1 Oct 2025 13:24:11 +0200 Subject: [PATCH 5/5] Fix syntax error --- tests/npmRegistryUtils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/npmRegistryUtils.test.ts b/tests/npmRegistryUtils.test.ts index eb21b8aa5..536534d9b 100644 --- a/tests/npmRegistryUtils.test.ts +++ b/tests/npmRegistryUtils.test.ts @@ -103,7 +103,7 @@ describe(`npm registry utils fetchAsJson`, () => { headers: { ...DEFAULT_HEADERS, authorization: `Basic ${encodedCreds}` - })); + }); }); it(`adds authorization header if COREPACK_NPM_PASSWORD is set and COREPACK_NPM_USERNAME is not.`, async () => {