From 6384ed4d118005944e287c19dc26cb2fd75930f3 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 12 Oct 2026 15:52:39 +0200 Subject: [PATCH] fix(packages/npm): restore former tarball URLs (#39681) Gitea changed the `dist.tarball` it advertises for already published versions twice: | Gitea | `dist.tarball` path of `@scope/name@1.0.0` | |---|---| | 1.27 | `%40scope%2Fname/-/1.0.0/name-1.0.0.tgz` | | 28.0, https://github.com/go-gitea/gitea/pull/39061 | `@scope%2Fname/-/1.0.0/name-1.0.0.tgz` | | 28.1, https://github.com/go-gitea/gitea/pull/39434 | `@scope/name/-/name-1.0.0.tgz` | pnpm 11.5.2+ verifies the tarball URLs pinned in its lockfile against the registry metadata, so frozen installs of lockfiles written against 1.27 fail with `ERR_PNPM_TARBALL_URL_MISMATCH`. - Versions uploaded before this PR get their 1.27 URL back. - New versions store their canonical tarball path at upload, so later URL changes can't reach published versions. The host still follows the request. Lockfiles written against 28.0 for scoped packages, or by pnpm 12 with `@scope:registry` against 28.1, need those pinned `tarball:` entries removed, as 1.27 lockfiles of the same versions need the other URL. Signed-off-by: silverwind --- modules/packages/npm/creator.go | 5 ++++- modules/packages/npm/creator_test.go | 5 +++-- modules/packages/npm/metadata.go | 2 ++ routers/api/packages/api.go | 2 +- routers/api/packages/npm/api.go | 8 +++++++- routers/api/packages/npm/api_test.go | 16 ++++++++++------ 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/modules/packages/npm/creator.go b/modules/packages/npm/creator.go index 6a40f876a50..b33df48df49 100644 --- a/modules/packages/npm/creator.go +++ b/modules/packages/npm/creator.go @@ -12,6 +12,7 @@ import ( "encoding/base64" "fmt" "io" + "net/url" "regexp" "strings" "sync" @@ -343,7 +344,9 @@ func parseUploadPackage(upload *packageUpload) (*Package, error) { p.DistTags = append(p.DistTags, tag) } - p.Filename = strings.ToLower(fmt.Sprintf("%s-%s.tgz", name, p.Version)) + filename := fmt.Sprintf("%s-%s.tgz", name, p.Version) + p.Filename = strings.ToLower(filename) + p.Metadata.GiteaTarballPath = util.PathEscapeSegments(p.Name) + "/-/" + url.PathEscape(filename) // canonical URL clients derive from name and version, file lookups ignore case attachment := upload.Attachments[meta.Name+"-"+meta.Version+".tgz"] // not the sigstore bundle of `npm publish --provenance` if attachment == nil && len(upload.Attachments) == 1 { diff --git a/modules/packages/npm/creator_test.go b/modules/packages/npm/creator_test.go index 983ad032b37..1728f5d0224 100644 --- a/modules/packages/npm/creator_test.go +++ b/modules/packages/npm/creator_test.go @@ -23,7 +23,7 @@ func TestParsePackage(t *testing.T) { packageScope := "@scope" packageName := "test-package" packageFullName := packageScope + "/" + packageName - packageVersion := "1.0.1-pre" + packageVersion := "1.0.1-Pre" packageTag := "latest" packageAuthor := "KN4CK3R" packageBin := "gitea" @@ -287,7 +287,8 @@ func TestParsePackage(t *testing.T) { assert.Equal(t, packageFullName, p.Name) assert.Equal(t, packageVersion, p.Version) assert.Equal(t, []string{packageTag}, p.DistTags) - assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageFullName, "/")[1], packageVersion), p.Filename) + assert.Equal(t, strings.ToLower(fmt.Sprintf("%s-%s.tgz", packageName, packageVersion)), p.Filename) + assert.Equal(t, fmt.Sprintf("%s/-/%s-%s.tgz", packageFullName, packageName, packageVersion), p.Metadata.GiteaTarballPath) b, _ = base64.StdEncoding.DecodeString(data) assert.Equal(t, b, p.Data) assert.Equal(t, packageName, p.Metadata.Name) diff --git a/modules/packages/npm/metadata.go b/modules/packages/npm/metadata.go index f0682d8934e..3e36a1151f2 100644 --- a/modules/packages/npm/metadata.go +++ b/modules/packages/npm/metadata.go @@ -34,4 +34,6 @@ type Metadata struct { Funding any `json:"funding,omitempty"` AcceptDependencies map[string]string `json:"accept_dependencies,omitempty"` Deprecated string `json:"deprecated,omitempty"` + + GiteaTarballPath string `json:"gitea_tarball_path,omitempty"` // Gitea-internal, fixed at upload because lockfiles pin it } diff --git a/routers/api/packages/api.go b/routers/api/packages/api.go index 63244a14159..ba7326bf519 100644 --- a/routers/api/packages/api.go +++ b/routers/api/packages/api.go @@ -414,7 +414,7 @@ func CommonRoutes() *web.Router { g.UseUnescapedPath() g.MatchPath("DELETE", packageId+"/-///-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) g.MatchPath("DELETE", packageId+"/-//-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion) - g.MatchPath("GET", packageId+"/-//", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles + g.MatchPath("GET", packageId+"/-//", npm.DownloadPackageFileByName) // 1.27 and 28.0 URL, still advertised for older uploads g.MatchPath("GET", packageId+"/-/", npm.DownloadPackageFileByName) g.MatchPath("DELETE", packageId+"/-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage) g.MatchPath("PUT", packageId+"/-rev/", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview) diff --git a/routers/api/packages/npm/api.go b/routers/api/packages/npm/api.go index 7640529feba..5eba148ab23 100644 --- a/routers/api/packages/npm/api.go +++ b/routers/api/packages/npm/api.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "encoding/hex" "fmt" + "net/url" "slices" "sort" "time" @@ -82,6 +83,11 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package metadata := packages_model.DescriptorMetadata[*npm_module.Metadata](pd) + tarballPath := metadata.GiteaTarballPath + if tarballPath == "" { + tarballPath = fmt.Sprintf("%s/-/%s/%s", url.QueryEscape(pd.Package.Name), url.PathEscape(pd.Version.Version), url.PathEscape(pd.Files[0].File.LowerName)) // 1.27 URL, pinned by older lockfiles + } + return &npm_module.PackageMetadataVersion{ ID: fmt.Sprintf("%s@%s", pd.Package.Name, pd.Version.Version), Name: pd.Package.Name, @@ -113,7 +119,7 @@ func createPackageMetadataVersion(registryURL string, pd *packages_model.Package Dist: npm_module.PackageDistribution{ Shasum: pd.Files[0].Blob.HashSHA1, Integrity: "sha512-" + base64.StdEncoding.EncodeToString(hashBytes), - Tarball: fmt.Sprintf("%s/%s/-/%s", registryURL, pd.Package.Name, pd.Files[0].File.LowerName), // npmjs shape, which npm parses for allowScripts and yarn keeps registry-relative + Tarball: registryURL + "/" + tarballPath, }, } } diff --git a/routers/api/packages/npm/api_test.go b/routers/api/packages/npm/api_test.go index a6e4d176ad6..22fce040185 100644 --- a/routers/api/packages/npm/api_test.go +++ b/routers/api/packages/npm/api_test.go @@ -19,13 +19,13 @@ import ( func TestCreatePackageMetadataResponse(t *testing.T) { repository := npm_module.Repository{Type: "git", URL: "https://gitea.dev/alice/test.git"} - descriptor := func(v string, publishedUnix int64, repo npm_module.Repository) *packages_model.PackageDescriptor { + descriptor := func(v string, publishedUnix int64, repo npm_module.Repository, tarballPath string) *packages_model.PackageDescriptor { return &packages_model.PackageDescriptor{ Package: &packages_model.Package{Name: "@scope/test"}, Owner: &user_model.User{Name: "alice"}, Version: &packages_model.PackageVersion{Version: v, CreatedUnix: timeutil.TimeStamp(publishedUnix)}, SemVer: version.Must(version.NewVersion(v)), - Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo}, + Metadata: &npm_module.Metadata{Readme: v, Keywords: []string{"gitea"}, Repository: repo, GiteaTarballPath: tarballPath}, Files: []*packages_model.PackageFileDescriptor{{ File: &packages_model.PackageFile{LowerName: "test-" + v + ".tgz"}, Blob: &packages_model.PackageBlob{}, @@ -34,9 +34,9 @@ func TestCreatePackageMetadataResponse(t *testing.T) { } result := createPackageMetadataResponse("https://gitea.dev/api/packages/alice/npm", []*packages_model.PackageDescriptor{ - descriptor("1.1.0", 1000, npm_module.Repository{}), - descriptor("2.0.0-rc.1", 1500, repository), - descriptor("1.0.0", 2000, repository), + descriptor("1.1.0", 1000, npm_module.Repository{}, "@scope/test/-/test-1.1.0.tgz"), + descriptor("2.0.0-rc.1", 1500, repository, ""), + descriptor("1.0.0", 2000, repository, ""), }) assert.Equal(t, map[string]time.Time{ @@ -54,9 +54,13 @@ func TestCreatePackageMetadataResponse(t *testing.T) { assert.Equal(t, []string{"gitea"}, result.Versions["1.0.0"].Keywords) assert.Equal(t, []npm_module.User{{Name: "alice"}}, result.Versions["1.0.0"].Maintainers) assert.Equal(t, - "https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.0.0.tgz", + "https://gitea.dev/api/packages/alice/npm/%40scope%2Ftest/-/1.0.0/test-1.0.0.tgz", result.Versions["1.0.0"].Dist.Tarball, ) + assert.Equal(t, + "https://gitea.dev/api/packages/alice/npm/@scope/test/-/test-1.1.0.tgz", + result.Versions["1.1.0"].Dist.Tarball, + ) assert.Equal(t, repository, result.Versions["1.0.0"].Repository) withoutRepository, err := json.Marshal(result.Versions["1.1.0"])