2026-03-11 17:18:29 +00:00
// Package swagger Code generated by swaggo/swag. DO NOT EDIT
package swagger
import "github.com/swaggo/swag"
const docTemplate = ` {
"schemes" : { { marshal . Schemes } } ,
"swagger" : "2.0" ,
"info" : {
"description" : "{{escape .Description}}" ,
"title" : "{{.Title}}" ,
"contact" : { } ,
"version" : "{{.Version}}"
} ,
"host" : "{{.Host}}" ,
"basePath" : "{{.BasePath}}" ,
"paths" : {
2026-09-03 05:06:44 -04:00
"/_internal/scan-fetch" : {
"get" : {
"description" : "Streams the exact bytes staged in storage for a pre-cache security scan.\nRequires a short-lived HMAC-signed token minted by the proxy itself and\ndelivered via the fetch_url field of the scan notify request (see the\nArtifact Scanning section of docs/configuration.md). Not part of the\npublic API; restrict access to the scanner network at the ingress layer." ,
"produces" : [
"application/octet-stream"
] ,
"tags" : [
"scanning"
] ,
"summary" : "Fetch a staged artifact for scanning" ,
"parameters" : [
{
"type" : "string" ,
"description" : "Storage path of the staged artifact" ,
"name" : "path" ,
"in" : "query" ,
"required" : true
} ,
{
"type" : "integer" ,
"description" : "Token expiry, Unix seconds" ,
"name" : "exp" ,
"in" : "query" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "HMAC-SHA256 signature over the string path|exp" ,
"name" : "sig" ,
"in" : "query" ,
"required" : true
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
"type" : "file"
}
} ,
"403" : {
"description" : "invalid, expired, or tampered token" ,
"schema" : {
"type" : "string"
}
} ,
"404" : {
"description" : "object not found in storage, or scanning is not configured" ,
"schema" : {
"type" : "string"
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/api/bulk" : {
"post" : {
"consumes" : [
"application/json"
] ,
2026-03-11 17:18:29 +00:00
"produces" : [
"application/json"
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"api"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Bulk package lookup by PURL" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
2026-06-07 15:47:29 +01:00
"description" : "PURLs" ,
"name" : "request" ,
"in" : "body" ,
"required" : true ,
"schema" : {
"$ref" : "#/definitions/server.BulkRequest"
}
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.BulkResponse"
2026-03-11 17:18:29 +00:00
}
} ,
2026-06-07 15:47:29 +01:00
"400" : {
"description" : "Bad Request" ,
2026-03-11 17:18:29 +00:00
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
Push, list and delete npm and Composer archives through /api/hosted
With hosted.enabled, PUT /api/hosted/{ecosystem}/{name}/{version} stores the request body as the archive for that version, marked local. It replaces any cached upstream copy and is served from the cache without contacting the upstream. GET /api/hosted/{ecosystem}/{name} lists what was pushed and DELETE removes a version again, after which clients get the upstream archive if there is one. Pushing and deleting need the bearer token; listing is open like the rest of the API.
Download handlers look archives up by the filename clients request, so a push is stored under the name its ecosystem derives from name and version: {name}-{version}.tgz for npm, without the scope, and {package}-{version}.zip for Composer. Other ecosystems are refused for now.
The Composer download handler resolves the upstream metadata before it looks at the cache and answers 404 for versions the upstream doesn't list. It now serves a pushed archive first. Only artifacts marked local take that path, so cached upstream archives still go through the metadata and commit checks.
The version's integrity is set to the pushed bytes, so the cache's integrity check verifies them rather than an upstream digest. Names and versions with path separators, dot segments or control characters are refused before anything is stored.
Refs #133
2026-10-13 13:11:49 +02:00
"/api/hosted/{ecosystem}/{name}" : {
"get" : {
"produces" : [
"application/json"
] ,
"tags" : [
"hosted"
] ,
"summary" : "List pushed package archives" ,
"parameters" : [
{
"type" : "string" ,
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Package name" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
"$ref" : "#/definitions/server.HostedListResponse"
}
} ,
"400" : {
"description" : "Bad Request" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
}
}
}
} ,
"/api/hosted/{ecosystem}/{name}/{version}" : {
"put" : {
"security" : [
{
"BearerAuth" : [ ]
}
] ,
"description" : "Stores the request body as the archive for name@version. It is served in place of the upstream archive with the same version and is never evicted." ,
"consumes" : [
"application/octet-stream"
] ,
"produces" : [
"application/json"
] ,
"tags" : [
"hosted"
] ,
"summary" : "Push a package archive" ,
"parameters" : [
{
"type" : "string" ,
"description" : "Ecosystem (npm or composer)" ,
"name" : "ecosystem" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Package name, including an npm scope or the Composer vendor" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Version" ,
"name" : "version" ,
"in" : "path" ,
"required" : true
}
] ,
"responses" : {
"201" : {
"description" : "Created" ,
"schema" : {
"$ref" : "#/definitions/server.HostedPushResponse"
}
} ,
"400" : {
"description" : "Bad Request" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
} ,
"401" : {
"description" : "Unauthorized" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
} ,
"403" : {
"description" : "Forbidden" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
} ,
"413" : {
"description" : "Request Entity Too Large" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
}
}
} ,
"delete" : {
"security" : [
{
"BearerAuth" : [ ]
}
] ,
"description" : "Removes the archives pushed for name@version. If the upstream has the version, clients get the upstream archive again." ,
"tags" : [
"hosted"
] ,
"summary" : "Delete a pushed version" ,
"parameters" : [
{
"type" : "string" ,
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Package name" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Version" ,
"name" : "version" ,
"in" : "path" ,
"required" : true
}
] ,
"responses" : {
"204" : {
"description" : "No Content"
} ,
"401" : {
"description" : "Unauthorized" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
} ,
"404" : {
"description" : "Not Found" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/api/outdated" : {
"post" : {
"consumes" : [
"application/json"
] ,
2026-03-11 17:18:29 +00:00
"produces" : [
2026-06-07 15:47:29 +01:00
"application/json"
2026-03-11 17:18:29 +00:00
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"api"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Check outdated packages" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
2026-06-07 15:47:29 +01:00
"description" : "Packages to check" ,
"name" : "request" ,
"in" : "body" ,
"required" : true ,
"schema" : {
"$ref" : "#/definitions/server.OutdatedRequest"
}
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.OutdatedResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"400" : {
"description" : "Bad Request" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/api/packages" : {
"get" : {
2026-03-11 17:18:29 +00:00
"produces" : [
"application/json"
] ,
"tags" : [
"api"
] ,
2026-06-07 15:47:29 +01:00
"summary" : "List cached packages" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
2026-06-07 15:47:29 +01:00
"type" : "string" ,
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "query"
} ,
{
"enum" : [
"hits" ,
"name" ,
"size" ,
"cached_at" ,
"ecosystem" ,
"vulns"
] ,
"type" : "string" ,
"description" : "Sort" ,
"name" : "sort" ,
"in" : "query"
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.PackagesListResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"400" : {
"description" : "Bad Request" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/api/search" : {
2026-03-11 17:18:29 +00:00
"get" : {
"produces" : [
"application/json"
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"api"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Search cached packages" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
"type" : "string" ,
2026-06-07 15:47:29 +01:00
"description" : "Query" ,
"name" : "q" ,
"in" : "query" ,
2026-03-11 17:18:29 +00:00
"required" : true
} ,
{
"type" : "string" ,
2026-06-07 15:47:29 +01:00
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "query"
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.SearchResponse"
2026-03-11 17:18:29 +00:00
}
} ,
2026-06-07 15:47:29 +01:00
"400" : {
"description" : "Bad Request" ,
2026-03-11 17:18:29 +00:00
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/health" : {
"get" : {
2026-03-11 17:18:29 +00:00
"produces" : [
"application/json"
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"meta"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Health check" ,
"responses" : {
"200" : {
"description" : "OK" ,
2026-03-11 17:18:29 +00:00
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.HealthResponse"
}
} ,
"503" : {
"description" : "Service Unavailable" ,
"schema" : {
"$ref" : "#/definitions/server.HealthResponse"
2026-03-11 17:18:29 +00:00
}
}
2026-06-07 15:47:29 +01:00
}
}
} ,
"/stats" : {
"get" : {
"produces" : [
"application/json"
] ,
"tags" : [
"meta"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Cache statistics" ,
2026-03-11 17:18:29 +00:00
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.StatsResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/ui/api/browse/{ecosystem}/{name}/{version}" : {
2026-03-11 17:18:29 +00:00
"get" : {
2026-06-07 15:47:29 +01:00
"description" : "Lists files from the first cached artifact for a package version." ,
2026-03-11 17:18:29 +00:00
"produces" : [
"application/json"
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"browse"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "List files inside a cached artifact" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
"type" : "string" ,
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
2026-06-07 15:47:29 +01:00
"in" : "path" ,
"required" : true
2026-03-11 17:18:29 +00:00
} ,
{
"type" : "string" ,
2026-06-07 15:47:29 +01:00
"description" : "Package name" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Version" ,
"name" : "version" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Directory path inside the archive" ,
"name" : "path" ,
2026-03-11 17:18:29 +00:00
"in" : "query"
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"$ref" : "#/definitions/server.BrowseListResponse"
2026-03-11 17:18:29 +00:00
}
} ,
2026-06-07 15:47:29 +01:00
"404" : {
"description" : "Not Found" ,
2026-03-11 17:18:29 +00:00
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/ui/api/browse/{ecosystem}/{name}/{version}/file/{filepath}" : {
2026-03-11 17:18:29 +00:00
"get" : {
2026-06-07 15:47:29 +01:00
"description" : "Streams a single file from the cached artifact. The file path may contain slashes." ,
2026-03-11 17:18:29 +00:00
"produces" : [
2026-06-07 15:47:29 +01:00
"application/octet-stream"
2026-03-11 17:18:29 +00:00
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"browse"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Fetch a file inside a cached artifact" ,
2026-03-11 17:18:29 +00:00
"parameters" : [
{
"type" : "string" ,
2026-06-07 15:47:29 +01:00
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "path" ,
2026-03-11 17:18:29 +00:00
"required" : true
} ,
{
"type" : "string" ,
2026-06-07 15:47:29 +01:00
"description" : "Package name" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "Version" ,
"name" : "version" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "File path inside the archive" ,
"name" : "filepath" ,
"in" : "path" ,
"required" : true
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"type" : "file"
2026-03-11 17:18:29 +00:00
}
} ,
"400" : {
"description" : "Bad Request" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
2026-06-07 15:47:29 +01:00
"404" : {
"description" : "Not Found" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
} ,
2026-03-11 17:18:29 +00:00
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
2026-06-07 15:47:29 +01:00
"/ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion}" : {
2026-03-11 17:18:29 +00:00
"get" : {
2026-06-07 15:47:29 +01:00
"description" : "Returns a structured diff for two cached versions." ,
2026-03-11 17:18:29 +00:00
"produces" : [
Add storage backend probe to /health (closes #73) (#119)
* config: add Health.StorageProbeInterval
* metrics: add proxy_health_probe_failures_total counter
* server: add storageProbe with happy-path test
* server: add storageProbe failure-mode tests
* server: add healthCache with TTL, single-flight, transition logging
* server: wire storage probe into /health
* server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer
Also fix Windows file-locking issue in storageProbe: close the reader
explicitly before Delete so the file handle is released prior to os.Remove.
* server: clean up stale comment in storageProbe
* docs: document storage health probe and new metric
* docs: regenerate Swagger for /health JSON response
* server: simplify rc.Close error handling in storageProbe
* server: defer probe cleanup so size/open/read/verify failures don't leak objects
Previously, storageProbe only called Delete on the success path. Any
failure between Store and the final Delete (size mismatch, Open error,
mid-stream read failure, content mismatch) left the probe object orphaned
in the storage backend. With caching disabled and Kubernetes-rate probing,
the leak could accumulate noticeably on backends like S3.
Use a named return + defer to attempt Delete after every successful Store.
The earlier-step failure remains the primary error; Delete failure only
surfaces as step="delete" when nothing else went wrong. Add a table-driven
test that asserts cleanup runs for each non-delete failure path.
Reported by Copilot on #119.
* config: validate health.storage_probe_interval in Config.Validate
The new duration field was only validated at use time in newHealthCache.
The existing codebase already validates other duration fields
(MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in
Config.Validate() so misconfiguration fails fast at startup with a
config-key-specific error.
Match that pattern. The parse-at-use code in newHealthCache stays as
a safety net, mirroring the MetadataTTL precedent.
Reported by Copilot on #119.
* docs: lowercase "counter" in metrics table for consistency
Other rows in the table use lowercase type names (counter/gauge/histogram).
Match that style.
Reported by Copilot on #119.
* docs: include size-check step in /health probe description
The probe is write → size-check → read → verify → delete; the
architecture note was missing the size-check step.
Reported by Copilot on #119.
* server: address andrew's review on #119
- Drop unused callerCtx parameter from healthCache.Check (Check is now
parameter-less; the comment-only "accepted for symmetry" justification
wasn't carrying its weight).
- Emit "storage": {"status": "skipped"} on DB short-circuit instead of
omitting the key, so monitors expecting a fixed key set keep working.
- Reject negative storage_probe_interval at config validation time
(previously parsed and silently behaved like "0").
- Extract HealthConfig.Validate to keep Config.Validate under the
gocognit threshold and match the existing GradleBuildCacheConfig pattern.
- README Health Check section: note that /health is intended as a
readiness probe rather than a liveness probe (Check holds a mutex
for up to the 10s probe timeout).
- cmd/proxy/main.go godoc: column-align the new env var with the
surrounding Gradle entries.
Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"application/json"
2026-03-11 17:18:29 +00:00
] ,
"tags" : [
2026-06-07 15:47:29 +01:00
"browse"
2026-03-11 17:18:29 +00:00
] ,
2026-06-07 15:47:29 +01:00
"summary" : "Compare two cached versions" ,
"parameters" : [
{
"type" : "string" ,
"description" : "Ecosystem" ,
"name" : "ecosystem" ,
"in" : "path" ,
"required" : true
2026-03-11 17:18:29 +00:00
} ,
2026-06-07 15:47:29 +01:00
{
"type" : "string" ,
"description" : "Package name" ,
"name" : "name" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "From version" ,
"name" : "fromVersion" ,
"in" : "path" ,
"required" : true
} ,
{
"type" : "string" ,
"description" : "To version" ,
"name" : "toVersion" ,
"in" : "path" ,
"required" : true
2026-03-11 17:18:29 +00:00
}
] ,
"responses" : {
"200" : {
"description" : "OK" ,
"schema" : {
2026-06-07 15:47:29 +01:00
"type" : "object" ,
"additionalProperties" : true
}
} ,
"404" : {
"description" : "Not Found" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
} ,
"500" : {
"description" : "Internal Server Error" ,
"schema" : {
2026-05-03 09:42:03 +01:00
"$ref" : "#/definitions/server.ErrorResponse"
2026-03-11 17:18:29 +00:00
}
2026-10-14 08:41:10 +02:00
} ,
"503" : {
"description" : "Service Unavailable" ,
"schema" : {
"$ref" : "#/definitions/server.ErrorResponse"
}
2026-03-11 17:18:29 +00:00
}
}
}
}
} ,
"definitions" : {
Push, list and delete npm and Composer archives through /api/hosted
With hosted.enabled, PUT /api/hosted/{ecosystem}/{name}/{version} stores the request body as the archive for that version, marked local. It replaces any cached upstream copy and is served from the cache without contacting the upstream. GET /api/hosted/{ecosystem}/{name} lists what was pushed and DELETE removes a version again, after which clients get the upstream archive if there is one. Pushing and deleting need the bearer token; listing is open like the rest of the API.
Download handlers look archives up by the filename clients request, so a push is stored under the name its ecosystem derives from name and version: {name}-{version}.tgz for npm, without the scope, and {package}-{version}.zip for Composer. Other ecosystems are refused for now.
The Composer download handler resolves the upstream metadata before it looks at the cache and answers 404 for versions the upstream doesn't list. It now serves a pushed archive first. Only artifacts marked local take that path, so cached upstream archives still go through the metadata and commit checks.
The version's integrity is set to the pushed bytes, so the cache's integrity check verifies them rather than an upstream digest. Names and versions with path separators, dot segments or control characters are refused before anything is stored.
Refs #133
2026-10-13 13:11:49 +02:00
"handler.HostedArtifact" : {
"type" : "object" ,
"properties" : {
"filename" : {
"type" : "string"
} ,
"pushed_at" : {
"type" : "string"
} ,
"sha256" : {
"type" : "string"
} ,
"size" : {
"type" : "integer"
} ,
"version" : {
"type" : "string"
}
}
} ,
2026-03-11 17:18:29 +00:00
"server.BrowseFileInfo" : {
"type" : "object" ,
"properties" : {
"is_dir" : {
"type" : "boolean"
} ,
"mod_time" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"path" : {
"type" : "string"
} ,
"size" : {
"type" : "integer"
}
}
} ,
"server.BrowseListResponse" : {
"type" : "object" ,
"properties" : {
"files" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.BrowseFileInfo"
}
} ,
"path" : {
"type" : "string"
}
}
} ,
"server.BulkRequest" : {
"type" : "object" ,
"properties" : {
"purls" : {
"type" : "array" ,
"items" : {
"type" : "string"
}
}
}
} ,
"server.BulkResponse" : {
"type" : "object" ,
"properties" : {
"packages" : {
"type" : "object" ,
"additionalProperties" : {
"$ref" : "#/definitions/server.PackageResponse"
}
}
}
} ,
2026-10-01 14:32:20 +02:00
"server.EcosystemStatsEntry" : {
"type" : "object" ,
"properties" : {
"cache_size_bytes" : {
"type" : "integer"
} ,
"cached_artifacts" : {
"type" : "integer"
} ,
"downloaded" : {
"type" : "string"
} ,
"downloaded_bytes" : {
"type" : "integer"
} ,
"downloads" : {
"type" : "integer"
} ,
"ecosystem" : {
"type" : "string"
} ,
"packages" : {
"type" : "integer"
} ,
"versions" : {
"type" : "integer"
}
}
} ,
2026-05-03 09:42:03 +01:00
"server.ErrorResponse" : {
"type" : "object" ,
"properties" : {
"code" : {
"type" : "string"
} ,
"message" : {
"type" : "string"
}
}
} ,
Add storage backend probe to /health (closes #73) (#119)
* config: add Health.StorageProbeInterval
* metrics: add proxy_health_probe_failures_total counter
* server: add storageProbe with happy-path test
* server: add storageProbe failure-mode tests
* server: add healthCache with TTL, single-flight, transition logging
* server: wire storage probe into /health
* server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer
Also fix Windows file-locking issue in storageProbe: close the reader
explicitly before Delete so the file handle is released prior to os.Remove.
* server: clean up stale comment in storageProbe
* docs: document storage health probe and new metric
* docs: regenerate Swagger for /health JSON response
* server: simplify rc.Close error handling in storageProbe
* server: defer probe cleanup so size/open/read/verify failures don't leak objects
Previously, storageProbe only called Delete on the success path. Any
failure between Store and the final Delete (size mismatch, Open error,
mid-stream read failure, content mismatch) left the probe object orphaned
in the storage backend. With caching disabled and Kubernetes-rate probing,
the leak could accumulate noticeably on backends like S3.
Use a named return + defer to attempt Delete after every successful Store.
The earlier-step failure remains the primary error; Delete failure only
surfaces as step="delete" when nothing else went wrong. Add a table-driven
test that asserts cleanup runs for each non-delete failure path.
Reported by Copilot on #119.
* config: validate health.storage_probe_interval in Config.Validate
The new duration field was only validated at use time in newHealthCache.
The existing codebase already validates other duration fields
(MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in
Config.Validate() so misconfiguration fails fast at startup with a
config-key-specific error.
Match that pattern. The parse-at-use code in newHealthCache stays as
a safety net, mirroring the MetadataTTL precedent.
Reported by Copilot on #119.
* docs: lowercase "counter" in metrics table for consistency
Other rows in the table use lowercase type names (counter/gauge/histogram).
Match that style.
Reported by Copilot on #119.
* docs: include size-check step in /health probe description
The probe is write → size-check → read → verify → delete; the
architecture note was missing the size-check step.
Reported by Copilot on #119.
* server: address andrew's review on #119
- Drop unused callerCtx parameter from healthCache.Check (Check is now
parameter-less; the comment-only "accepted for symmetry" justification
wasn't carrying its weight).
- Emit "storage": {"status": "skipped"} on DB short-circuit instead of
omitting the key, so monitors expecting a fixed key set keep working.
- Reject negative storage_probe_interval at config validation time
(previously parsed and silently behaved like "0").
- Extract HealthConfig.Validate to keep Config.Validate under the
gocognit threshold and match the existing GradleBuildCacheConfig pattern.
- README Health Check section: note that /health is intended as a
readiness probe rather than a liveness probe (Check holds a mutex
for up to the 10s probe timeout).
- cmd/proxy/main.go godoc: column-align the new env var with the
surrounding Gradle entries.
Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"server.HealthCheck" : {
"type" : "object" ,
"properties" : {
"error" : {
"type" : "string"
} ,
"status" : {
"type" : "string"
} ,
"step" : {
"type" : "string"
}
}
} ,
"server.HealthResponse" : {
"type" : "object" ,
"properties" : {
"checks" : {
"type" : "object" ,
"additionalProperties" : {
"$ref" : "#/definitions/server.HealthCheck"
}
} ,
2026-09-02 13:52:32 +02:00
"circuit_breakers" : {
"description" : "CircuitBreakers reports the state (\"open\" or \"closed\") of each upstream\nregistry's artifact-fetch circuit breaker, keyed by the host fetched from\nor, where the fetch URL has none to read, by an opaque placeholder\nstanding in for it. It is omitted when no breaker has been created yet.\nAn open breaker fails every artifact fetch it covers without contacting\nthe upstream, but says nothing about this proxy's own health, so it does\nnot change Status." ,
"type" : "object" ,
"additionalProperties" : {
"type" : "string"
}
} ,
Add storage backend probe to /health (closes #73) (#119)
* config: add Health.StorageProbeInterval
* metrics: add proxy_health_probe_failures_total counter
* server: add storageProbe with happy-path test
* server: add storageProbe failure-mode tests
* server: add healthCache with TTL, single-flight, transition logging
* server: wire storage probe into /health
* server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer
Also fix Windows file-locking issue in storageProbe: close the reader
explicitly before Delete so the file handle is released prior to os.Remove.
* server: clean up stale comment in storageProbe
* docs: document storage health probe and new metric
* docs: regenerate Swagger for /health JSON response
* server: simplify rc.Close error handling in storageProbe
* server: defer probe cleanup so size/open/read/verify failures don't leak objects
Previously, storageProbe only called Delete on the success path. Any
failure between Store and the final Delete (size mismatch, Open error,
mid-stream read failure, content mismatch) left the probe object orphaned
in the storage backend. With caching disabled and Kubernetes-rate probing,
the leak could accumulate noticeably on backends like S3.
Use a named return + defer to attempt Delete after every successful Store.
The earlier-step failure remains the primary error; Delete failure only
surfaces as step="delete" when nothing else went wrong. Add a table-driven
test that asserts cleanup runs for each non-delete failure path.
Reported by Copilot on #119.
* config: validate health.storage_probe_interval in Config.Validate
The new duration field was only validated at use time in newHealthCache.
The existing codebase already validates other duration fields
(MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in
Config.Validate() so misconfiguration fails fast at startup with a
config-key-specific error.
Match that pattern. The parse-at-use code in newHealthCache stays as
a safety net, mirroring the MetadataTTL precedent.
Reported by Copilot on #119.
* docs: lowercase "counter" in metrics table for consistency
Other rows in the table use lowercase type names (counter/gauge/histogram).
Match that style.
Reported by Copilot on #119.
* docs: include size-check step in /health probe description
The probe is write → size-check → read → verify → delete; the
architecture note was missing the size-check step.
Reported by Copilot on #119.
* server: address andrew's review on #119
- Drop unused callerCtx parameter from healthCache.Check (Check is now
parameter-less; the comment-only "accepted for symmetry" justification
wasn't carrying its weight).
- Emit "storage": {"status": "skipped"} on DB short-circuit instead of
omitting the key, so monitors expecting a fixed key set keep working.
- Reject negative storage_probe_interval at config validation time
(previously parsed and silently behaved like "0").
- Extract HealthConfig.Validate to keep Config.Validate under the
gocognit threshold and match the existing GradleBuildCacheConfig pattern.
- README Health Check section: note that /health is intended as a
readiness probe rather than a liveness probe (Check holds a mutex
for up to the 10s probe timeout).
- cmd/proxy/main.go godoc: column-align the new env var with the
surrounding Gradle entries.
Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
"status" : {
"type" : "string"
2026-10-14 12:51:43 +01:00
} ,
"upstream_holds" : {
"description" : "UpstreamHolds lists upstream hosts that rate limited the proxy and\nthe RFC 3339 time until which it sends them nothing. A held host\nfails cache misses without contacting it and does not change Status." ,
"type" : "object" ,
"additionalProperties" : {
"type" : "string"
}
Add storage backend probe to /health (closes #73) (#119)
* config: add Health.StorageProbeInterval
* metrics: add proxy_health_probe_failures_total counter
* server: add storageProbe with happy-path test
* server: add storageProbe failure-mode tests
* server: add healthCache with TTL, single-flight, transition logging
* server: wire storage probe into /health
* server: update TestHealthEndpoint for JSON; wire healthCache into newTestServer
Also fix Windows file-locking issue in storageProbe: close the reader
explicitly before Delete so the file handle is released prior to os.Remove.
* server: clean up stale comment in storageProbe
* docs: document storage health probe and new metric
* docs: regenerate Swagger for /health JSON response
* server: simplify rc.Close error handling in storageProbe
* server: defer probe cleanup so size/open/read/verify failures don't leak objects
Previously, storageProbe only called Delete on the success path. Any
failure between Store and the final Delete (size mismatch, Open error,
mid-stream read failure, content mismatch) left the probe object orphaned
in the storage backend. With caching disabled and Kubernetes-rate probing,
the leak could accumulate noticeably on backends like S3.
Use a named return + defer to attempt Delete after every successful Store.
The earlier-step failure remains the primary error; Delete failure only
surfaces as step="delete" when nothing else went wrong. Add a table-driven
test that asserts cleanup runs for each non-delete failure path.
Reported by Copilot on #119.
* config: validate health.storage_probe_interval in Config.Validate
The new duration field was only validated at use time in newHealthCache.
The existing codebase already validates other duration fields
(MetadataTTL, DirectServeTTL, Gradle.MaxAge, Gradle.SweepInterval) in
Config.Validate() so misconfiguration fails fast at startup with a
config-key-specific error.
Match that pattern. The parse-at-use code in newHealthCache stays as
a safety net, mirroring the MetadataTTL precedent.
Reported by Copilot on #119.
* docs: lowercase "counter" in metrics table for consistency
Other rows in the table use lowercase type names (counter/gauge/histogram).
Match that style.
Reported by Copilot on #119.
* docs: include size-check step in /health probe description
The probe is write → size-check → read → verify → delete; the
architecture note was missing the size-check step.
Reported by Copilot on #119.
* server: address andrew's review on #119
- Drop unused callerCtx parameter from healthCache.Check (Check is now
parameter-less; the comment-only "accepted for symmetry" justification
wasn't carrying its weight).
- Emit "storage": {"status": "skipped"} on DB short-circuit instead of
omitting the key, so monitors expecting a fixed key set keep working.
- Reject negative storage_probe_interval at config validation time
(previously parsed and silently behaved like "0").
- Extract HealthConfig.Validate to keep Config.Validate under the
gocognit threshold and match the existing GradleBuildCacheConfig pattern.
- README Health Check section: note that /health is intended as a
readiness probe rather than a liveness probe (Check holds a mutex
for up to the 10s probe timeout).
- cmd/proxy/main.go godoc: column-align the new env var with the
surrounding Gradle entries.
Reported by andrew on #119.
2026-05-22 14:14:01 +03:00
}
}
} ,
Push, list and delete npm and Composer archives through /api/hosted
With hosted.enabled, PUT /api/hosted/{ecosystem}/{name}/{version} stores the request body as the archive for that version, marked local. It replaces any cached upstream copy and is served from the cache without contacting the upstream. GET /api/hosted/{ecosystem}/{name} lists what was pushed and DELETE removes a version again, after which clients get the upstream archive if there is one. Pushing and deleting need the bearer token; listing is open like the rest of the API.
Download handlers look archives up by the filename clients request, so a push is stored under the name its ecosystem derives from name and version: {name}-{version}.tgz for npm, without the scope, and {package}-{version}.zip for Composer. Other ecosystems are refused for now.
The Composer download handler resolves the upstream metadata before it looks at the cache and answers 404 for versions the upstream doesn't list. It now serves a pushed archive first. Only artifacts marked local take that path, so cached upstream archives still go through the metadata and commit checks.
The version's integrity is set to the pushed bytes, so the cache's integrity check verifies them rather than an upstream digest. Names and versions with path separators, dot segments or control characters are refused before anything is stored.
Refs #133
2026-10-13 13:11:49 +02:00
"server.HostedListResponse" : {
"type" : "object" ,
"properties" : {
"artifacts" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/handler.HostedArtifact"
}
} ,
"ecosystem" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
}
}
} ,
"server.HostedPushResponse" : {
"type" : "object" ,
"properties" : {
"ecosystem" : {
"type" : "string"
} ,
"filename" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"pushed_at" : {
"type" : "string"
} ,
"sha256" : {
"type" : "string"
} ,
"size" : {
"type" : "integer"
} ,
"version" : {
"type" : "string"
}
}
} ,
2026-03-11 17:18:29 +00:00
"server.OutdatedPackage" : {
"type" : "object" ,
"properties" : {
"ecosystem" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"version" : {
"type" : "string"
}
}
} ,
"server.OutdatedRequest" : {
"type" : "object" ,
"properties" : {
"packages" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.OutdatedPackage"
}
}
}
} ,
"server.OutdatedResponse" : {
"type" : "object" ,
"properties" : {
"results" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.OutdatedResult"
}
}
}
} ,
"server.OutdatedResult" : {
"type" : "object" ,
"properties" : {
"ecosystem" : {
"type" : "string"
} ,
"is_outdated" : {
"type" : "boolean"
} ,
"latest_version" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"version" : {
"type" : "string"
}
}
} ,
"server.PackageListResult" : {
"type" : "object" ,
"properties" : {
"cached_at" : {
"type" : "string"
} ,
"ecosystem" : {
"type" : "string"
} ,
"hits" : {
"type" : "integer"
} ,
"latest_version" : {
"type" : "string"
} ,
"license" : {
"type" : "string"
} ,
"license_category" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"size" : {
"type" : "integer"
} ,
"vuln_count" : {
"type" : "integer"
}
}
} ,
"server.PackageResponse" : {
"type" : "object" ,
"properties" : {
"description" : {
"type" : "string"
} ,
"ecosystem" : {
"type" : "string"
} ,
"homepage" : {
"type" : "string"
} ,
"latest_version" : {
"type" : "string"
} ,
"license" : {
"type" : "string"
} ,
"license_category" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"registry_url" : {
"type" : "string"
} ,
"repository" : {
"type" : "string"
}
}
} ,
"server.PackagesListResponse" : {
"type" : "object" ,
"properties" : {
"count" : {
"type" : "integer"
} ,
"ecosystem" : {
"type" : "string"
} ,
"page" : {
"type" : "integer"
} ,
"per_page" : {
"type" : "integer"
} ,
"results" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.PackageListResult"
}
} ,
"sort_by" : {
"type" : "string"
} ,
"total" : {
"type" : "integer"
}
}
} ,
"server.SearchPackageResult" : {
"type" : "object" ,
"properties" : {
"cached_at" : {
"type" : "string"
} ,
"ecosystem" : {
"type" : "string"
} ,
"hits" : {
"type" : "integer"
} ,
"latest_version" : {
"type" : "string"
} ,
"license" : {
"type" : "string"
} ,
"name" : {
"type" : "string"
} ,
"size" : {
"type" : "integer"
}
}
} ,
"server.SearchResponse" : {
"type" : "object" ,
"properties" : {
"count" : {
"type" : "integer"
} ,
"query" : {
"type" : "string"
} ,
"results" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.SearchPackageResult"
}
}
}
} ,
"server.StatsResponse" : {
"type" : "object" ,
"properties" : {
"cached_artifacts" : {
"type" : "integer"
} ,
"database_path" : {
"type" : "string"
} ,
2026-10-01 14:32:20 +02:00
"downloaded" : {
"type" : "string"
} ,
"downloaded_bytes" : {
"description" : "DownloadedBytes is the accumulated download volume across every\necosystem: cache hits multiplied by the artifact size they served." ,
"type" : "integer"
} ,
"downloads" : {
"type" : "integer"
} ,
"ecosystems" : {
"type" : "array" ,
"items" : {
"$ref" : "#/definitions/server.EcosystemStatsEntry"
}
} ,
"stats_unavailable" : {
"description" : "StatsUnavailable distinguishes a proxy that has served nothing from one\nwhose aggregation failed with no snapshot to fall back on. Without it\nboth report zeros and an empty array." ,
"type" : "boolean"
} ,
2026-04-03 14:06:51 +01:00
"storage_url" : {
2026-03-11 17:18:29 +00:00
"type" : "string"
} ,
"total_size" : {
"type" : "string"
} ,
"total_size_bytes" : {
"type" : "integer"
}
}
}
Push, list and delete npm and Composer archives through /api/hosted
With hosted.enabled, PUT /api/hosted/{ecosystem}/{name}/{version} stores the request body as the archive for that version, marked local. It replaces any cached upstream copy and is served from the cache without contacting the upstream. GET /api/hosted/{ecosystem}/{name} lists what was pushed and DELETE removes a version again, after which clients get the upstream archive if there is one. Pushing and deleting need the bearer token; listing is open like the rest of the API.
Download handlers look archives up by the filename clients request, so a push is stored under the name its ecosystem derives from name and version: {name}-{version}.tgz for npm, without the scope, and {package}-{version}.zip for Composer. Other ecosystems are refused for now.
The Composer download handler resolves the upstream metadata before it looks at the cache and answers 404 for versions the upstream doesn't list. It now serves a pushed archive first. Only artifacts marked local take that path, so cached upstream archives still go through the metadata and commit checks.
The version's integrity is set to the pushed bytes, so the cache's integrity check verifies them rather than an upstream digest. Names and versions with path separators, dot segments or control characters are refused before anything is stored.
Refs #133
2026-10-13 13:11:49 +02:00
} ,
"securityDefinitions" : {
"BearerAuth" : {
"description" : "\"Bearer \" followed by hosted.token" ,
"type" : "apiKey" ,
"name" : "Authorization" ,
"in" : "header"
}
2026-03-11 17:18:29 +00:00
}
} `
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = & swag . Spec {
Version : "0.1.0" ,
Host : "" ,
BasePath : "/" ,
Schemes : [ ] string { } ,
Title : "git-pkgs proxy API" ,
Description : "HTTP API for package enrichment, vulnerability lookup, cache stats, and source browsing." ,
InfoInstanceName : "swagger" ,
SwaggerTemplate : docTemplate ,
LeftDelim : "{{" ,
RightDelim : "}}" ,
}
func init ( ) {
swag . Register ( SwaggerInfo . InstanceName ( ) , SwaggerInfo )
}