Skip to content

Commit ba0d868

Browse files
committed
fix: address review feedback for registry-url auth
- Split .npmrc on /\r?\n/ instead of EOL to handle cross-platform line endings correctly - Filter existing _authToken lines for the same registry to prevent duplicate conflicting entries - Use URL hostname comparison instead of includes() for GitHub Packages auto-detection to prevent spoofing - Fix action.yml description to accurately describe $RUNNER_TEMP/.npmrc behavior
1 parent f14ab3c commit ba0d868

File tree

4 files changed

+69
-16
lines changed

4 files changed

+69
-16
lines changed

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ inputs:
2828
description: "Path to lock file for cache key generation. Auto-detected if not specified."
2929
required: false
3030
registry-url:
31-
description: "Optional registry to set up for auth. Will set the registry in a project level .npmrc and set up auth to read in from env.NODE_AUTH_TOKEN."
31+
description: "Optional registry to set up for auth. Will write a .npmrc in $RUNNER_TEMP with the registry URL and auth token config, and point npm at it via NPM_CONFIG_USERCONFIG."
3232
required: false
3333
scope:
3434
description: "Optional scope for authenticating against scoped registries. Will fall back to the repository owner when using the GitHub Packages registry (https://npm.pkg.github.com/)."

dist/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,4 +211,4 @@ ${e.format(t)}
211211
`));let i=t.split(`
212212
`).map(e=>e.trim());for(let e of i)if(!e||e.startsWith(`#`))continue;else r.patterns.push(new m.Pattern(e));return r.searchPaths.push(...f.getSearchPaths(r.patterns)),r})}static stat(e,t,n){return i(this,void 0,void 0,function*(){let r;if(t.followSymbolicLinks)try{r=yield l.promises.stat(e.path)}catch(n){if(n.code===`ENOENT`){if(t.omitBrokenSymbolicLinks){c.debug(`Broken symlink '${e.path}'`);return}throw Error(`No information found for the path '${e.path}'. This may indicate a broken symbolic link.`)}throw n}else r=yield l.promises.lstat(e.path);if(r.isDirectory()&&t.followSymbolicLinks){let t=yield l.promises.realpath(e.path);for(;n.length>=e.level;)n.pop();if(n.some(e=>e===t)){c.debug(`Symlink cycle detected for path '${e.path}' and realpath '${t}'`);return}n.push(t)}return r})}}})),dm=v((e=>{var t=e&&e.__createBinding||(Object.create?(function(e,t,n,r){r===void 0&&(r=n);var i=Object.getOwnPropertyDescriptor(t,n);(!i||(`get`in i?!t.__esModule:i.writable||i.configurable))&&(i={enumerable:!0,get:function(){return t[n]}}),Object.defineProperty(e,r,i)}):(function(e,t,n,r){r===void 0&&(r=n),e[r]=t[n]})),n=e&&e.__setModuleDefault||(Object.create?(function(e,t){Object.defineProperty(e,`default`,{enumerable:!0,value:t})}):function(e,t){e.default=t}),r=e&&e.__importStar||(function(){var e=function(t){return e=Object.getOwnPropertyNames||function(e){var t=[];for(var n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[t.length]=n);return t},e(t)};return function(r){if(r&&r.__esModule)return r;var i={};if(r!=null)for(var a=e(r),o=0;o<a.length;o++)a[o]!==`default`&&t(i,r,a[o]);return n(i,r),i}})(),i=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})},a=e&&e.__asyncValues||function(e){if(!Symbol.asyncIterator)throw TypeError(`Symbol.asyncIterator is not defined.`);var t=e[Symbol.asyncIterator],n;return t?t.call(e):(e=typeof __values==`function`?__values(e):e[Symbol.iterator](),n={},r(`next`),r(`throw`),r(`return`),n[Symbol.asyncIterator]=function(){return this},n);function r(t){n[t]=e[t]&&function(n){return new Promise(function(r,a){n=e[t](n),i(r,a,n.done,n.value)})}}function i(e,t,n,r){Promise.resolve(r).then(function(t){e({value:t,done:n})},t)}};Object.defineProperty(e,`__esModule`,{value:!0}),e.hashFiles=f;let o=r(S(`crypto`)),s=r(nm()),c=r(S(`fs`)),l=r(S(`stream`)),u=r(S(`util`)),d=r(S(`path`));function f(e,t){return i(this,arguments,void 0,function*(e,t,n=!1){var r,i,f,p;let m=n?s.info:s.debug,h=!1,g=t||(process.env.GITHUB_WORKSPACE??process.cwd()),_=o.createHash(`sha256`),v=0;try{for(var y=!0,b=a(e.globGenerator()),x;x=yield b.next(),r=x.done,!r;y=!0){p=x.value,y=!1;let e=p;if(m(e),!e.startsWith(`${g}${d.sep}`)){m(`Ignore '${e}' since it is not under GITHUB_WORKSPACE.`);continue}if(c.statSync(e).isDirectory()){m(`Skip directory '${e}'.`);continue}let t=o.createHash(`sha256`);yield u.promisify(l.pipeline)(c.createReadStream(e),t),_.write(t.digest()),v++,h||=!0}}catch(e){i={error:e}}finally{try{!y&&!r&&(f=b.return)&&(yield f.call(b))}finally{if(i)throw i.error}}return _.end(),h?(m(`Found ${v} files to hash.`),_.digest(`hex`)):(m(`No matches found for glob`),``)})}})),fm=v((e=>{var t=e&&e.__awaiter||function(e,t,n,r){function i(e){return e instanceof n?e:new n(function(t){t(e)})}return new(n||=Promise)(function(n,a){function o(e){try{c(r.next(e))}catch(e){a(e)}}function s(e){try{c(r.throw(e))}catch(e){a(e)}}function c(e){e.done?n(e.value):i(e.value).then(o,s)}c((r=r.apply(e,t||[])).next())})};Object.defineProperty(e,`__esModule`,{value:!0}),e.hashFiles=a;let n=um(),r=dm();function i(e,r){return t(this,void 0,void 0,function*(){return yield n.DefaultGlobber.create(e,r)})}function a(e){return t(this,arguments,void 0,function*(e,t=``,n,a=!1){let o=!0;n&&typeof n.followSymbolicLinks==`boolean`&&(o=n.followSymbolicLinks);let s=yield i(e,{followSymbolicLinks:o});return(0,r.hashFiles)(s,t,a)})}}))();async function pm(e){let t=Vd(e.cacheDependencyPath);if(!t){(0,$.warning)(`No lock file found. Skipping cache restore.`),(0,$.setOutput)(Li.CacheHit,!1);return}(0,$.info)(`Using lock file: ${t.path}`);let n=await Ud(t.type);if(!n.length){(0,$.warning)(`No cache directories found. Skipping cache restore.`),(0,$.setOutput)(Li.CacheHit,!1);return}(0,$.debug)(`Cache paths: ${n.join(`, `)}`),(0,$.saveState)(Ii.CachePaths,JSON.stringify(n));let r=process.env.RUNNER_OS||c(),i=o(),a=await(0,fm.hashFiles)(t.path);if(!a)throw Error(`Failed to generate hash for lock file: ${t.path}`);let s=`vite-plus-${r}-${i}-${t.type}-${a}`,l=[`vite-plus-${r}-${i}-${t.type}-`,`vite-plus-${r}-${i}-`];(0,$.debug)(`Primary key: ${s}`),(0,$.debug)(`Restore keys: ${l.join(`, `)}`),(0,$.saveState)(Ii.CachePrimaryKey,s);let u=await(0,Id.restoreCache)(n,s,l);u?((0,$.info)(`Cache restored from key: ${u}`),(0,$.saveState)(Ii.CacheMatchedKey,u),(0,$.setOutput)(Li.CacheHit,!0)):((0,$.info)(`Cache not found`),(0,$.setOutput)(Li.CacheHit,!1))}async function mm(){let e=(0,$.getState)(Ii.CachePrimaryKey),t=(0,$.getState)(Ii.CacheMatchedKey),n=(0,$.getState)(Ii.CachePaths);if(!e){(0,$.info)(`No cache key found. Skipping cache save.`);return}if(!n){(0,$.info)(`No cache paths found. Skipping cache save.`);return}if(e===t){(0,$.info)(`Cache hit on primary key "${e}". Skipping save.`);return}let r=JSON.parse(n);if(!r.length){(0,$.info)(`Empty cache paths. Skipping cache save.`);return}try{if(await(0,Id.saveCache)(r,e)===-1){(0,$.warning)(`Cache save failed or was skipped.`);return}(0,$.info)(`Cache saved with key: ${e}`)}catch(e){(0,$.warning)(`Failed to save cache: ${String(e)}`)}}function hm(e){let n=zd(e),r;try{r=u(n,`utf-8`)}catch{throw Error(`node-version-file not found: ${n}`)}let i=t(n),a;if(a=i===`.tool-versions`?vm(r):i===`package.json`?bm(r):gm(r),!a)throw Error(`No Node.js version found in ${e}`);return a=a.replace(/^v/i,``),(0,$.info)(`Resolved Node.js version '${a}' from ${e}`),a}function gm(e){for(let t of e.split(`
213213
`)){let e=(t.includes(`#`)?t.slice(0,t.indexOf(`#`)):t).trim();if(e)return _m(e)}}function _m(e){let t=e.toLowerCase();return t===`node`||t===`stable`?`latest`:e}function vm(e){for(let t of e.split(`
214-
`)){let e=t.trim();if(!e||e.startsWith(`#`))continue;let[n,...r]=e.split(/\s+/);if(!(n!==`nodejs`&&n!==`node`)){for(let e of r)if(ym(e))return e}}}function ym(e){return!!e&&e!==`system`&&!e.startsWith(`ref:`)&&!e.startsWith(`path:`)}function bm(e){let t;try{t=JSON.parse(e)}catch{throw Error(`Failed to parse package.json: invalid JSON`)}let n=t.devEngines;if(n?.runtime){let e=xm(n.runtime);if(e)return e}let r=t.engines;if(r?.node&&typeof r.node==`string`)return r.node}function xm(e){let t=Array.isArray(e)?e:[e];for(let e of t)if(e?.name===`node`&&typeof e.version==`string`)return e.version}function Sm(e,t){let n=i(process.env.RUNNER_TEMP||process.cwd(),`.npmrc`);e.endsWith(`/`)||(e+=`/`),Cm(e,n,t)}function Cm(e,t,n){!n&&e.includes(`npm.pkg.github.com`)&&(n=process.env.GITHUB_REPOSITORY_OWNER),n&&!n.startsWith(`@`)&&(n=`@`+n),n=n?n.toLowerCase()+`:`:``,(0,$.debug)(`Setting auth in ${t}`);let r=``;if(l(t)){let e=u(t,`utf8`);for(let t of e.split(a))t.toLowerCase().startsWith(`${n}registry`)||(r+=t+a)}let i=e.replace(/^\w+:/,``)+":_authToken=${NODE_AUTH_TOKEN}",o=`${n}registry=${e}`;r+=`${i}${a}${o}`,f(t,r),(0,$.exportVariable)(`NPM_CONFIG_USERCONFIG`,t),(0,$.exportVariable)(`NODE_AUTH_TOKEN`,process.env.NODE_AUTH_TOKEN||`XXXXX-XXXXX-XXXXX-XXXXX`)}async function wm(e){(0,$.saveState)(Ii.IsPost,`true`);let t=e.nodeVersion;!t&&e.nodeVersionFile&&(t=hm(e.nodeVersionFile)),await Xd(e,t||``),t&&((0,$.info)(`Setting up Node.js ${t} via vp env use...`),await(0,Mi.exec)(`vp`,[`env`,`use`,t])),e.registryUrl&&Sm(e.registryUrl,e.scope),e.cache&&await pm(e),e.runInstall.length>0&&await Qd(e),await Tm()}async function Tm(){try{let e=(await(0,Mi.getExecOutput)(`vp`,[`--version`],{silent:!0})).stdout.trim();(0,$.info)(e);let t=e.match(/Global:\s*v?([\d.]+[^\s]*)/i)?.[1]||`unknown`;(0,$.saveState)(Ii.InstalledVersion,t),(0,$.setOutput)(Li.Version,t)}catch(e){(0,$.warning)(`Could not get vp version: ${String(e)}`),(0,$.setOutput)(Li.Version,`unknown`)}}async function Em(e){let t=[Yd()];e.cache&&t.push(mm()),await Promise.all(t)}async function Dm(){let e=zi();(0,$.getState)(Ii.IsPost)===`true`?await Em(e):await wm(e)}Dm().catch(e=>{console.error(e),(0,$.setFailed)(e instanceof Error?e.message:String(e))});export{};
214+
`)){let e=t.trim();if(!e||e.startsWith(`#`))continue;let[n,...r]=e.split(/\s+/);if(!(n!==`nodejs`&&n!==`node`)){for(let e of r)if(ym(e))return e}}}function ym(e){return!!e&&e!==`system`&&!e.startsWith(`ref:`)&&!e.startsWith(`path:`)}function bm(e){let t;try{t=JSON.parse(e)}catch{throw Error(`Failed to parse package.json: invalid JSON`)}let n=t.devEngines;if(n?.runtime){let e=xm(n.runtime);if(e)return e}let r=t.engines;if(r?.node&&typeof r.node==`string`)return r.node}function xm(e){let t=Array.isArray(e)?e:[e];for(let e of t)if(e?.name===`node`&&typeof e.version==`string`)return e.version}function Sm(e,t){let n=i(process.env.RUNNER_TEMP||process.cwd(),`.npmrc`);e.endsWith(`/`)||(e+=`/`),Cm(e,n,t)}function Cm(e,t,n){if(!n)try{new URL(e).hostname===`npm.pkg.github.com`&&(n=process.env.GITHUB_REPOSITORY_OWNER)}catch{}n&&!n.startsWith(`@`)&&(n=`@`+n),n=n?n.toLowerCase()+`:`:``,(0,$.debug)(`Setting auth in ${t}`);let r=e.replace(/^\w+:/,``).toLowerCase(),i=[];if(l(t)){let e=u(t,`utf8`);for(let t of e.split(/\r?\n/)){let e=t.toLowerCase();e.startsWith(`${n}registry`)||e.startsWith(r)&&e.includes(`_authtoken`)||i.push(t)}}let o=e.replace(/^\w+:/,``)+":_authToken=${NODE_AUTH_TOKEN}",s=`${n}registry=${e}`;i.push(o,s),f(t,i.join(a)),(0,$.exportVariable)(`NPM_CONFIG_USERCONFIG`,t),(0,$.exportVariable)(`NODE_AUTH_TOKEN`,process.env.NODE_AUTH_TOKEN||`XXXXX-XXXXX-XXXXX-XXXXX`)}async function wm(e){(0,$.saveState)(Ii.IsPost,`true`);let t=e.nodeVersion;!t&&e.nodeVersionFile&&(t=hm(e.nodeVersionFile)),await Xd(e,t||``),t&&((0,$.info)(`Setting up Node.js ${t} via vp env use...`),await(0,Mi.exec)(`vp`,[`env`,`use`,t])),e.registryUrl&&Sm(e.registryUrl,e.scope),e.cache&&await pm(e),e.runInstall.length>0&&await Qd(e),await Tm()}async function Tm(){try{let e=(await(0,Mi.getExecOutput)(`vp`,[`--version`],{silent:!0})).stdout.trim();(0,$.info)(e);let t=e.match(/Global:\s*v?([\d.]+[^\s]*)/i)?.[1]||`unknown`;(0,$.saveState)(Ii.InstalledVersion,t),(0,$.setOutput)(Li.Version,t)}catch(e){(0,$.warning)(`Could not get vp version: ${String(e)}`),(0,$.setOutput)(Li.Version,`unknown`)}}async function Em(e){let t=[Yd()];e.cache&&t.push(mm()),await Promise.all(t)}async function Dm(){let e=zi();(0,$.getState)(Ii.IsPost)===`true`?await Em(e):await wm(e)}Dm().catch(e=>{console.error(e),(0,$.setFailed)(e instanceof Error?e.message:String(e))});export{};

src/auth.test.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { describe, it, expect, beforeEach, afterEach, vi } from "vite-plus/test";
22
import { join } from "node:path";
33
import { existsSync, readFileSync, writeFileSync } from "node:fs";
4-
import { EOL } from "node:os";
54
import { configAuthentication } from "./auth.js";
65
import { exportVariable } from "@actions/core";
76

@@ -94,10 +93,14 @@ describe("configAuthentication", () => {
9493
);
9594
});
9695

97-
it("should preserve existing .npmrc content except registry lines", () => {
96+
it("should preserve existing .npmrc content except registry and auth lines", () => {
9897
vi.mocked(existsSync).mockReturnValue(true);
9998
vi.mocked(readFileSync).mockReturnValue(
100-
`always-auth=true${EOL}registry=https://old.reg/${EOL}`,
99+
[
100+
"always-auth=true",
101+
"registry=https://old.reg/",
102+
"//old.reg/:_authToken=${NODE_AUTH_TOKEN}",
103+
].join("\n"),
101104
);
102105

103106
configAuthentication("https://registry.npmjs.org/");
@@ -108,6 +111,45 @@ describe("configAuthentication", () => {
108111
expect(written).toContain("registry=https://registry.npmjs.org/");
109112
});
110113

114+
it("should remove existing auth token lines for the same registry", () => {
115+
vi.mocked(existsSync).mockReturnValue(true);
116+
vi.mocked(readFileSync).mockReturnValue(
117+
[
118+
"//registry.npmjs.org/:_authToken=old-token",
119+
"registry=https://registry.npmjs.org/",
120+
"other-config=true",
121+
].join("\n"),
122+
);
123+
124+
configAuthentication("https://registry.npmjs.org/");
125+
126+
const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string;
127+
expect(written).not.toContain("old-token");
128+
expect(written).toContain("other-config=true");
129+
expect(written).toContain("//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}");
130+
});
131+
132+
it("should handle Windows-style line endings in existing .npmrc", () => {
133+
vi.mocked(existsSync).mockReturnValue(true);
134+
vi.mocked(readFileSync).mockReturnValue("always-auth=true\r\nregistry=https://old.reg/\r\n");
135+
136+
configAuthentication("https://registry.npmjs.org/");
137+
138+
const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string;
139+
expect(written).toContain("always-auth=true");
140+
expect(written).not.toContain("https://old.reg/");
141+
});
142+
143+
it("should not auto-detect scope for lookalike GitHub Packages URLs", () => {
144+
vi.stubEnv("GITHUB_REPOSITORY_OWNER", "voidzero-dev");
145+
146+
configAuthentication("https://npm.pkg.github.com.evil.example");
147+
148+
const written = vi.mocked(writeFileSync).mock.calls[0]![1] as string;
149+
// Should NOT have scoped registry — the host doesn't match exactly
150+
expect(written).not.toContain("@voidzero-dev:");
151+
});
152+
111153
it("should export NPM_CONFIG_USERCONFIG", () => {
112154
configAuthentication("https://registry.npmjs.org/");
113155

src/auth.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ export function configAuthentication(registryUrl: string, scope?: string): void
1818
}
1919

2020
function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?: string): void {
21-
// Auto-detect scope for GitHub Packages registry
22-
if (!scope && registryUrl.includes("npm.pkg.github.com")) {
23-
scope = process.env.GITHUB_REPOSITORY_OWNER;
21+
// Auto-detect scope for GitHub Packages registry using exact host match
22+
if (!scope) {
23+
try {
24+
const url = new URL(registryUrl);
25+
if (url.hostname === "npm.pkg.github.com") {
26+
scope = process.env.GITHUB_REPOSITORY_OWNER;
27+
}
28+
} catch {
29+
// Invalid URL — skip auto-detection
30+
}
2431
}
2532

2633
if (scope && !scope.startsWith("@")) {
@@ -35,23 +42,27 @@ function writeRegistryToFile(registryUrl: string, fileLocation: string, scope?:
3542

3643
debug(`Setting auth in ${fileLocation}`);
3744

38-
let newContents = "";
45+
// Compute the auth line prefix for filtering existing entries
46+
const authPrefix = registryUrl.replace(/^\w+:/, "").toLowerCase();
47+
48+
const lines: string[] = [];
3949
if (existsSync(fileLocation)) {
4050
const curContents = readFileSync(fileLocation, "utf8");
41-
for (const line of curContents.split(EOL)) {
42-
// Preserve lines that don't set the scoped registry
43-
if (!line.toLowerCase().startsWith(`${scope}registry`)) {
44-
newContents += line + EOL;
45-
}
51+
for (const line of curContents.split(/\r?\n/)) {
52+
const lower = line.toLowerCase();
53+
// Remove existing registry and auth token lines for this scope/registry
54+
if (lower.startsWith(`${scope}registry`)) continue;
55+
if (lower.startsWith(authPrefix) && lower.includes("_authtoken")) continue;
56+
lines.push(line);
4657
}
4758
}
4859

4960
// Auth token line: remove protocol prefix from registry URL
5061
const authString = registryUrl.replace(/^\w+:/, "") + ":_authToken=${NODE_AUTH_TOKEN}";
5162
const registryString = `${scope}registry=${registryUrl}`;
52-
newContents += `${authString}${EOL}${registryString}`;
63+
lines.push(authString, registryString);
5364

54-
writeFileSync(fileLocation, newContents);
65+
writeFileSync(fileLocation, lines.join(EOL));
5566

5667
exportVariable("NPM_CONFIG_USERCONFIG", fileLocation);
5768
// Export placeholder if NODE_AUTH_TOKEN is not set so npm doesn't error

0 commit comments

Comments
 (0)