Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 07:39:19 -05:00
pkg-proxy/internal/config/hosted.go
simon chrzanowski 69313c26d9
Serve hosted-only npm scopes and Composer vendors from pushed packages alone
A package the upstream doesn't know is served from its pushed versions, but the upstream is still asked about it. Once someone publishes the same name there, their versions are listed next to the pushed ones and a version range can resolve to them.

hosted.npm_scopes and hosted.composer_vendors (PROXY_HOSTED_NPM_SCOPES, PROXY_HOSTED_COMPOSER_VENDORS) name namespaces whose packages are served from pushed versions alone. Their metadata is built from what was pushed, npm latest included, and anything not pushed answers 404. The upstream is never asked about them. Packages in other namespaces are proxied as before.

The npm metadata handler's decisions about pushed versions move into serveLocalMetadata.

Refs #133
Refs #134
2026-10-14 13:23:19 +02:00

77 lines
2.6 KiB
Go

package config
import (
"fmt"
"strings"
)
const defaultHostedMaxUploadSize = "100MB"
// HostedConfig configures packages pushed to the proxy through the
// /api/hosted endpoints. Pushed versions are served in place of upstream
// versions with the same number, so writes always require a token.
type HostedConfig struct {
// Enabled registers the /api/hosted endpoints. Off by default.
Enabled bool `json:"enabled" yaml:"enabled"`
// Token is the bearer token required to push and delete packages.
// Required when Enabled. Supports ${VAR_NAME} expansion like AuthConfig
// fields.
Token string `json:"token" yaml:"token"`
// MaxUploadSize caps a single pushed archive (e.g., "100MB"). Must be > 0.
// Default: "100MB".
MaxUploadSize string `json:"max_upload_size" yaml:"max_upload_size"`
// NPMScopes are npm scopes (e.g. "@acme") served only from pushed
// packages. The upstream registry is never asked about them, so a package
// published there under the same name can't be mixed in.
NPMScopes []string `json:"npm_scopes" yaml:"npm_scopes"`
// ComposerVendors are Composer vendors (e.g. "acme") served only from
// pushed packages, like NPMScopes.
ComposerVendors []string `json:"composer_vendors" yaml:"composer_vendors"`
maxUploadBytes int64
}
// TokenExpanded returns Token with ${VAR_NAME} references expanded.
func (h *HostedConfig) TokenExpanded() string {
return expandEnv(h.Token)
}
// MaxUploadBytes returns the parsed MaxUploadSize. Validate must run first.
func (h *HostedConfig) MaxUploadBytes() int64 {
return h.maxUploadBytes
}
// Validate checks the hosted package settings, applying the default upload
// size if unset.
func (h *HostedConfig) Validate() error {
if h.MaxUploadSize == "" {
h.MaxUploadSize = defaultHostedMaxUploadSize
}
size, err := ParseSize(h.MaxUploadSize)
if err != nil {
return fmt.Errorf("invalid hosted.max_upload_size: %w", err)
}
if size <= 0 {
return fmt.Errorf("invalid hosted.max_upload_size %q: must be > 0", h.MaxUploadSize)
}
h.maxUploadBytes = size
if h.Enabled && h.TokenExpanded() == "" {
return fmt.Errorf("hosted.token is required when hosted.enabled is true")
}
for _, scope := range h.NPMScopes {
if len(scope) < 2 || scope[0] != '@' || strings.Contains(scope, "/") {
return fmt.Errorf("invalid hosted.npm_scopes entry %q: must be a scope such as \"@acme\"", scope)
}
}
for _, vendor := range h.ComposerVendors {
if vendor == "" || strings.Contains(vendor, "/") {
return fmt.Errorf("invalid hosted.composer_vendors entry %q: must be a vendor such as \"acme\"", vendor)
}
}
return nil
}