Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 23:59:16 -05:00
pkg-proxy/internal/handler/npm_hosted.go
Andrew Nesbitt 8ed0edb90a Extract portable npm metadata transformations
npm metadata rewriting depended on handler.Proxy, which pulls SQL
drivers, cloud SDKs and metrics into its import graph. Move the
transformation into internal/metadata/npm with explicit policy,
evaluation time, public URL and hosted-version inputs, and share the
JSON scanner between npm and Composer through internal/jsonscan.

Rebased onto main: rewritten tarball URLs are still logged once per
document, now through an OnTarballsRewritten callback that reports the
count.
2026-10-25 22:21:37 +01:00

194 lines
6.7 KiB
Go

package handler
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
"github.com/Masterminds/semver/v3"
"github.com/git-pkgs/proxy/internal/metadata/npm"
)
type npmLocalEntry = npm.LocalVersion
// npmLocalEntries returns the pushed versions of a package as packument
// entries, or nil when there are none or hosted packages are disabled.
func (h *NPMHandler) npmLocalEntries(packageName string) ([]npmLocalEntry, error) {
local, err := h.proxy.localVersions("npm", packageName)
if err != nil || len(local) == 0 {
return nil, err
}
entries := make([]npmLocalEntry, 0, len(local))
for _, lv := range local {
var manifest map[string]any
if err := json.Unmarshal(lv.Manifest, &manifest); err != nil {
return nil, fmt.Errorf("decoding manifest of %s@%s: %w", packageName, lv.Version, err)
}
manifest["name"] = packageName
manifest["version"] = lv.Version
manifest["_id"] = packageName + "@" + lv.Version
manifest["dist"] = map[string]string{
"tarball": h.proxyTarballURL(packageName, lv.Version, lv.Filename),
"integrity": lv.Integrity,
}
entry, err := json.Marshal(manifest)
if err != nil {
return nil, err
}
entries = append(entries, npmLocalEntry{Version: lv.Version, Entry: entry, PublishedAt: lv.PushedAt.Format(time.RFC3339)})
}
return entries, nil
}
func findNPMLocalEntry(local []npmLocalEntry, version string) (npmLocalEntry, bool) {
for _, e := range local {
if e.Version == version {
return e, true
}
}
return npmLocalEntry{}, false
}
// npmMetadataRequest is a packument or version metadata request.
type npmMetadataRequest struct {
packageName string
specifier string
versionLookup bool
upstreamURL string
accept string
metadataCacheKey string
}
// serveLocalMetadata answers the metadata requests that pushed versions
// decide, and reports whether it did: a request for a pushed version, any
// request for a package in a hosted-only scope, and a packument of a package
// with pushed versions. Everything else takes the usual upstream path.
func (h *NPMHandler) serveLocalMetadata(w http.ResponseWriter, r *http.Request, req npmMetadataRequest) bool {
local, err := h.npmLocalEntries(req.packageName)
if err != nil {
h.proxy.Logger.Error("failed to read pushed npm versions", "package", req.packageName, "error", err)
JSONError(w, http.StatusInternalServerError, "failed to read pushed versions")
return true
}
switch e, pushed := findNPMLocalEntry(local, req.specifier); {
case req.versionLookup && pushed:
w.Header().Set(headerContentType, contentTypeJSON)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(e.Entry)
case h.proxy.hostedOnly("npm", req.packageName):
h.serveHostedOnlyMetadata(w, r, req.packageName, req.specifier, req.versionLookup, req.metadataCacheKey, local)
case len(local) > 0 && !req.versionLookup:
h.serveMetadataWithLocal(w, r, req.packageName, req.upstreamURL, req.accept, req.metadataCacheKey, local)
default:
return false
}
return true
}
// serveMetadataWithLocal serves a packument with the pushed versions of the
// package added. It bypasses the rewrite cache, whose entries are keyed by the
// upstream document alone, so a push or delete shows up on the next request.
// When the upstream does not know the package, the packument holds only the
// pushed versions.
func (h *NPMHandler) serveMetadataWithLocal(w http.ResponseWriter, r *http.Request, packageName, upstreamURL, accept, metadataCacheKey string, local []npmLocalEntry) {
upstream := h.proxy.fetchMetadataDocument(r.Context(), "npm", metadataCacheKey, upstreamURL, accept, h.packumentValidator())
var out []byte
var err error
switch {
case errors.Is(upstream.err, ErrUpstreamNotFound):
out, err = localNPMPackument(packageName, local)
case upstream.err != nil:
if writeUpstreamHeld(w, upstream.err) {
return
}
h.proxy.Logger.Error("failed to fetch npm metadata", "error", upstream.err)
JSONError(w, http.StatusBadGateway, "failed to fetch from upstream")
return
default:
out, _, err = h.rewriteMetadataWithLocal(packageName, upstream.body, local)
}
if err != nil {
if errors.Is(err, ErrVersionDenied) {
JSONError(w, http.StatusForbidden, err.Error())
return
}
h.proxy.Logger.Error("failed to add pushed versions to npm metadata", "package", packageName, "error", err)
JSONError(w, http.StatusBadGateway, "failed to rewrite package metadata")
return
}
markStale(w, upstream.stale)
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("npm", h.proxyURL, metadataCacheKey, out))
}
// serveHostedOnlyMetadata answers a metadata request for a package in a
// hosted-only scope from its pushed versions alone.
func (h *NPMHandler) serveHostedOnlyMetadata(w http.ResponseWriter, r *http.Request, packageName, specifier string, versionLookup bool, metadataCacheKey string, local []npmLocalEntry) {
if len(local) == 0 {
JSONError(w, http.StatusNotFound, ErrPackageNotFound.Error())
return
}
if versionLookup {
if specifier == "latest" {
specifier = latestNPMVersion(local)
}
e, ok := findNPMLocalEntry(local, specifier)
if !ok {
JSONError(w, http.StatusNotFound, ErrVersionNotFound.Error())
return
}
w.Header().Set(headerContentType, contentTypeJSON)
w.WriteHeader(http.StatusOK)
_, _ = w.Write(e.Entry)
return
}
out, err := localNPMPackument(packageName, local)
if err != nil {
h.proxy.Logger.Error("failed to build npm metadata from pushed versions", "package", packageName, "error", err)
JSONError(w, http.StatusInternalServerError, "failed to build package metadata")
return
}
serveRewrittenMetadata(w, r, h.proxy.uncachedRewrite("npm", h.proxyURL, metadataCacheKey, out))
}
// localNPMPackument builds a packument from pushed versions alone.
func localNPMPackument(packageName string, local []npmLocalEntry) ([]byte, error) {
versions := make(map[string]json.RawMessage, len(local))
times := make(map[string]string, len(local))
for _, e := range local {
versions[e.Version] = e.Entry
times[e.Version] = e.PublishedAt
}
return json.Marshal(map[string]any{
"name": packageName,
"dist-tags": map[string]string{"latest": latestNPMVersion(local)},
"versions": versions,
"time": times,
})
}
// latestNPMVersion returns the highest pushed version, or the last pushed
// when a version is not semver.
func latestNPMVersion(local []npmLocalEntry) string {
latest, latestTime := "", ""
var latestSemver *semver.Version
semverOnly := true
for _, e := range local {
if e.PublishedAt > latestTime {
latest, latestTime = e.Version, e.PublishedAt
}
v, err := semver.NewVersion(e.Version)
if err != nil {
semverOnly = false
continue
}
if latestSemver == nil || v.GreaterThan(latestSemver) {
latestSemver = v
}
}
if semverOnly && latestSemver != nil {
return latestSemver.Original()
}
return latest
}