From 37e4c5be4a8724a9aeea03788c753512a671469a Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 21 Mar 2026 11:30:06 +0000 Subject: [PATCH] fix(npm): decode URL-encoded package names after regex extraction Scoped npm packages use %2f encoding for the slash in URLs (e.g. @scope%2fpackage). Previously, the encoded name was used as-is for storage and packument metadata, causing npm install to fail with EINVALIDPACKAGENAME. Now each regex extraction point decodes the package name via decodeURIComponent while keeping the path encoded for correct regex matching. --- ts/npm/classes.npmregistry.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/ts/npm/classes.npmregistry.ts b/ts/npm/classes.npmregistry.ts index 0a39668..479092a 100644 --- a/ts/npm/classes.npmregistry.ts +++ b/ts/npm/classes.npmregistry.ts @@ -155,45 +155,45 @@ export class NpmRegistry extends BaseRegistry { // Dist-tags: /-/package/{package}/dist-tags const distTagsMatch = path.match(/^\/-\/package\/(@?[^\/]+(?:\/[^\/]+)?)\/dist-tags(?:\/(.+))?$/); if (distTagsMatch) { - const [, packageName, tag] = distTagsMatch; - return this.handleDistTags(context.method, packageName, tag, context.body, token); + const [, rawPkgName, tag] = distTagsMatch; + return this.handleDistTags(context.method, decodeURIComponent(rawPkgName), tag, context.body, token); } // Tarball download: /{package}/-/{filename}.tgz const tarballMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/(.+\.tgz)$/); if (tarballMatch) { - const [, packageName, filename] = tarballMatch; - return this.handleTarballDownload(packageName, filename, token, actor); + const [, rawPkgName, filename] = tarballMatch; + return this.handleTarballDownload(decodeURIComponent(rawPkgName), filename, token, actor); } // Unpublish specific version: DELETE /{package}/-/{version} const unpublishVersionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-\/([^\/]+)$/); if (unpublishVersionMatch && context.method === 'DELETE') { - const [, packageName, version] = unpublishVersionMatch; - this.logger.log('debug', 'unpublishVersionMatch', { packageName, version }); - return this.unpublishVersion(packageName, version, token); + const [, rawPkgName, version] = unpublishVersionMatch; + this.logger.log('debug', 'unpublishVersionMatch', { packageName: decodeURIComponent(rawPkgName), version }); + return this.unpublishVersion(decodeURIComponent(rawPkgName), version, token); } // Unpublish entire package: DELETE /{package}/-rev/{rev} const unpublishPackageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/-rev\/([^\/]+)$/); if (unpublishPackageMatch && context.method === 'DELETE') { - const [, packageName, rev] = unpublishPackageMatch; - this.logger.log('debug', 'unpublishPackageMatch', { packageName, rev }); - return this.unpublishPackage(packageName, token); + const [, rawPkgName, rev] = unpublishPackageMatch; + this.logger.log('debug', 'unpublishPackageMatch', { packageName: decodeURIComponent(rawPkgName), rev }); + return this.unpublishPackage(decodeURIComponent(rawPkgName), token); } // Package version: /{package}/{version} const versionMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)\/([^\/]+)$/); if (versionMatch) { - const [, packageName, version] = versionMatch; - this.logger.log('debug', 'versionMatch', { packageName, version }); - return this.handlePackageVersion(packageName, version, token, actor); + const [, rawPkgName, version] = versionMatch; + this.logger.log('debug', 'versionMatch', { packageName: decodeURIComponent(rawPkgName), version }); + return this.handlePackageVersion(decodeURIComponent(rawPkgName), version, token, actor); } // Package operations: /{package} const packageMatch = path.match(/^\/(@?[^\/]+(?:\/[^\/]+)?)$/); if (packageMatch) { - const packageName = packageMatch[1]; + const packageName = decodeURIComponent(packageMatch[1]); this.logger.log('debug', 'packageMatch', { packageName }); return this.handlePackage(context.method, packageName, context.body, context.query, token, actor); }