Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ cooldown:
# npm: "7d"
# cargo: "0"

# Per-package overrides (keyed by PURL)
# Per-package overrides (keyed by PURL). Keys are normalized, so npm scopes
# may use either @scope or the canonical %40scope form.
# packages:
# "pkg:npm/lodash": "0"
# "pkg:npm/@babel/core": "14d"
2 changes: 2 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ cooldown:

Durations support days (`7d`), hours (`48h`), and minutes (`30m`). Set to `0` to disable.

Package PURL keys are normalized automatically. For npm scopes, both the readable form (`pkg:npm/@babel/core`) and canonical form (`pkg:npm/%40babel/core`) are accepted. If both forms configure the same package, the canonical entry wins.

Resolution order: package override, then ecosystem override, then global default. This lets you set a conservative default while exempting trusted packages.

Currently supported for npm, PyPI, pub.dev, Composer, Cargo, NuGet, Conda, RubyGems, and Hex. These ecosystems include publish timestamps in their metadata.
Expand Down
31 changes: 31 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,12 @@ import (
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"

"github.com/git-pkgs/purl"
"gopkg.in/yaml.v3"
)

Expand Down Expand Up @@ -137,9 +139,38 @@ type CooldownConfig struct {
Ecosystems map[string]string `json:"ecosystems" yaml:"ecosystems"`

// Packages overrides the cooldown for specific packages (keyed by PURL).
// Valid PURL keys are normalized to canonical form before use.
Packages map[string]string `json:"packages" yaml:"packages"`
}

// NormalizedPackages returns a copy of the package overrides with valid PURL
// keys in canonical form. An explicitly canonical key wins over an equivalent
// noncanonical key, and invalid keys are preserved unchanged.
func (c *CooldownConfig) NormalizedPackages() map[string]string {
if c == nil || c.Packages == nil {
return nil
}

keys := make([]string, 0, len(c.Packages))
for key := range c.Packages {
keys = append(keys, key)
}
sort.Strings(keys)

normalized := make(map[string]string, len(c.Packages))
for _, key := range keys {
canonical := key
if parsed, err := purl.Parse(key); err == nil {
canonical = parsed.String()
}
if _, exists := normalized[canonical]; exists && key != canonical {
continue
}
normalized[canonical] = c.Packages[key]
}
return normalized
}

// StorageConfig configures artifact storage.
type StorageConfig struct {
// URL is the storage backend URL.
Expand Down
28 changes: 28 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,34 @@ cooldown:
if cfg.Cooldown.Packages["pkg:npm/@babel/core"] != "14d" {
t.Errorf("Cooldown.Packages[@babel/core] = %q, want %q", cfg.Cooldown.Packages["pkg:npm/@babel/core"], "14d")
}
if got := cfg.Cooldown.NormalizedPackages()["pkg:npm/%40babel/core"]; got != "14d" {
t.Errorf("normalized Cooldown.Packages[@babel/core] = %q, want %q", got, "14d")
}
}

func TestCooldownConfigNormalizedPackages(t *testing.T) {
rawScoped := "pkg:npm/@typescript/typescript-darwin-arm64"
canonicalScoped := "pkg:npm/%40typescript/typescript-darwin-arm64"
cfg := CooldownConfig{Packages: map[string]string{
rawScoped: "2d",
canonicalScoped: "3d",
"not-a-purl": "4d",
}}

got := cfg.NormalizedPackages()

if got[canonicalScoped] != "3d" {
t.Errorf("canonical scoped package duration = %q, want %q", got[canonicalScoped], "3d")
}
if _, exists := got[rawScoped]; exists {
t.Errorf("raw scoped package key %q was not canonicalized", rawScoped)
}
if got["not-a-purl"] != "4d" {
t.Errorf("invalid PURL duration = %q, want preserved value %q", got["not-a-purl"], "4d")
}
if cfg.Packages[rawScoped] != "2d" {
t.Error("NormalizedPackages mutated the source map")
}
}

func TestLoadCooldownFromEnv(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func (s *Server) Start() error {
cd := &cooldown.Config{
Default: s.cfg.Cooldown.Default,
Ecosystems: s.cfg.Cooldown.Ecosystems,
Packages: s.cfg.Cooldown.Packages,
Packages: s.cfg.Cooldown.NormalizedPackages(),
}
proxy := handler.NewProxy(s.db, s.storage, fetcher, resolver, s.logger)
proxy.HTTPClient.Timeout = s.cfg.ParseHTTPTimeout()
Expand Down
Loading