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 <me@silverwind.io>
This commit is contained in:
silverwind authored and GitHub committed 2026-10-12 13:52:39 +00:00
1 parent 2f1761e3fe
commit 6384ed4d11
6 files changed
+27 -11

No files matched your search

+1 -1
View File
@@ -414,7 +414,7 @@ func CommonRoutes() *web.Router {
g.UseUnescapedPath()
g.MatchPath("DELETE", packageId+"/-/<version>/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
g.MatchPath("DELETE", packageId+"/-/<filename>/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackageVersion)
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // former tarball URL, still in lockfiles
g.MatchPath("GET", packageId+"/-/<version>/<filename>", npm.DownloadPackageFileByName) // 1.27 and 28.0 URL, still advertised for older uploads
g.MatchPath("GET", packageId+"/-/<filename>", npm.DownloadPackageFileByName)
g.MatchPath("DELETE", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePackage)
g.MatchPath("PUT", packageId+"/-rev/<revision>", reqPackageAccess(perm.AccessModeWrite), npm.DeletePreview)
+7 -1
View File
@@ -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,
},
}
}
+10 -6
View File
@@ -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"])