Documentation
¶
Overview ¶
Package pin is the public Go API for the pin tool: read a manifest, resolve assets, write them out, and emit a CycloneDX lockfile.
Index ¶
- Constants
- Variables
- func EncodeLock(l *lock.Lock) ([]byte, error)
- func Init(dir, manifestPath string) error
- func OutdatedExitCode(reports []OutdatedReport) int
- func Path(name string, opts VerifyOptions) ([]string, error)
- func SBOM(w io.Writer, opts SBOMOptions) error
- type AddOptions
- type AddResult
- type Client
- func (c *Client) Add(ctx context.Context, spec string, files []string, opts AddOptions) (*AddResult, error)
- func (c *Client) Outdated(ctx context.Context, opts OutdatedOptions) ([]OutdatedReport, error)
- func (c *Client) RegisterResolver(purlType string, r source.Resolver)
- func (c *Client) Remove(ctx context.Context, names []string, opts SyncOptions) (*SyncResult, error)
- func (c *Client) Resolver(purlType string) source.Resolver
- func (c *Client) Sync(ctx context.Context, opts SyncOptions) (*SyncResult, error)
- func (c *Client) Verify(opts VerifyOptions) (*VerifyResult, error)
- type ClientOptions
- type Drift
- type ListEntry
- type OutdatedOptions
- type OutdatedReport
- type SBOMFormat
- type SBOMOptions
- type SyncOptions
- type SyncResult
- type VerifyOptions
- type VerifyResult
Examples ¶
Constants ¶
const ( SeverityOK = "ok" SeverityBehind = "behind" SeverityDeprecated = "deprecated" SeverityYanked = "yanked" SeverityProvenanceDowngrade = "provenance-downgrade" )
const ( ExitOutdated = 7 ExitYanked = 9 )
const ( DefaultManifest = "pin.yaml" DefaultLock = "pin.lock" ToolName = "pin" )
Variables ¶
var ( ErrNoLockfile = errors.New("no lockfile; run sync first") ErrFrozenDrift = errors.New("manifest and lockfile disagree") ErrVerifyFailed = errors.New("verify failed") ErrProvenanceMissing = errors.New("no attestation recorded") // ErrPublisherMismatch fires when the attestation's // source_repository does not match the package's declared // repository.url and no trusted_workflows entry matches. ErrPublisherMismatch = errors.New("attestation source repository mismatch") ErrPathEscape = errors.New("output path escapes out directory") // ErrPathCollision fires when two resolved assets in the same // Sync produce the same on-disk Out. Most common under // layout: flat when two packages or two files within one entry // share a basename. pin fails closed rather than silently // overwriting. ErrPathCollision = errors.New("two assets resolve to the same output path") )
Sentinel errors for branching with errors.Is.
CLI exit-code mapping:
ErrFrozenDrift, ErrVerifyFailed → exit 4 ErrProvenanceMissing, ErrPublisherMismatch → exit 4 OutdatedExitCode handles yanked / behind / deprecated → 7 or 9
var ToolVersion = "dev"
ToolVersion is overridden at build time via the `-X github.com/git-pkgs/pin.ToolVersion=X.Y.Z` ldflag.
Functions ¶
func EncodeLock ¶
EncodeLock returns the lockfile bytes for l using the current tool name and version.
Example ¶
ExampleEncodeLock serialises a Lock value to the CycloneDX-shaped pin.lock bytes. Useful when generating a lockfile programmatically without going through Sync.
package main
import (
"encoding/json"
"fmt"
pin "github.com/git-pkgs/pin"
"github.com/git-pkgs/pin/lock"
)
func main() {
l := &lock.Lock{
LockfileVersion: 1,
OutDir: "vendor",
Assets: []lock.Asset{{
Name: "demo",
Version: "1.0.0",
PURL: "pkg:npm/demo@1.0.0",
Out: "demo/dist/demo.js",
}},
}
b, err := pin.EncodeLock(l)
if err != nil {
panic(err)
}
var top struct {
BOMFormat string `json:"bomFormat"`
}
_ = json.Unmarshal(b, &top)
fmt.Println(top.BOMFormat)
}
Output: CycloneDX
func Init ¶
Init writes a starter pin.yaml in dir. Fails if the destination already exists; pin will not overwrite an existing manifest.
Example ¶
ExampleInit writes a starter pin.yaml in an empty project. The manifest path argument is optional; passing "" picks the default.
package main
import (
"fmt"
"os"
"path/filepath"
pin "github.com/git-pkgs/pin"
)
func main() {
dir, err := os.MkdirTemp("", "pin-example-init-")
if err != nil {
panic(err)
}
defer func() { _ = os.RemoveAll(dir) }()
if err := pin.Init(dir, ""); err != nil {
panic(err)
}
_, err = os.Stat(filepath.Join(dir, pin.DefaultManifest))
fmt.Println(err == nil)
}
Output: true
func OutdatedExitCode ¶
func OutdatedExitCode(reports []OutdatedReport) int
OutdatedExitCode collapses reports into the CLI exit code: ExitYanked (9) > ExitOutdated (7) > 0.
Example ¶
ExampleOutdatedExitCode mirrors what the `pin outdated` CLI returns to the shell: 9 if any package is yanked, 7 if any is behind or deprecated, 0 otherwise.
package main
import (
"fmt"
pin "github.com/git-pkgs/pin"
)
func main() {
reports := []pin.OutdatedReport{
{Name: "a", Behind: true},
{Name: "b", Yanked: true},
}
fmt.Println(pin.OutdatedExitCode(reports))
}
Output: 9
Types ¶
type AddOptions ¶
type AddResult ¶
type AddResult struct {
Entry manifest.Entry
Resolved string
SyncResult *SyncResult
}
AddResult has SyncResult == nil under --dry-run.
type Client ¶
type Client struct {
// NPM, Forge, URL stay pointing at the resolvers registered by
// New even when a consumer overrides the same purl type via
// RegisterResolver. Operations that need source-specific APIs
// (npm.IsSticky, npm.Status, npm tarball re-derive for
// verify --strict) use these.
NPM *npm.Source
Forge *forge.Source
URL *rawurl.Source
// contains filtered or unexported fields
}
Client holds shared state across pin operations: HTTP client, source resolvers keyed by purl type, and typed accessors for the built-in sources. Safe for concurrent use across operations.
Example ¶
ExampleClient shows the reusable-client path: construct once via New, register a custom resolver for a non-built-in purl type, then drive multiple operations off the same Client. The package-level pin.Sync shim builds a fresh Client per call; long-running consumers prefer this style.
package main
import (
"fmt"
pin "github.com/git-pkgs/pin"
)
func main() {
c := pin.New(pin.ClientOptions{
RegistryURL: "https://registry.npmjs.org",
})
// Plug in a resolver for pkg:ipfs/... entries without forking pin.
// c.RegisterResolver("ipfs", myIPFSResolver)
fmt.Println(c.Resolver("npm") != nil)
fmt.Println(c.Resolver("ipfs") != nil)
}
Output: true false
func New ¶
func New(opts ClientOptions) *Client
New returns a Client with the built-in npm, github, and generic resolvers registered. Consumers can add or replace resolvers via RegisterResolver before calling any operation method.
func (*Client) Add ¶
func (c *Client) Add(ctx context.Context, spec string, files []string, opts AddOptions) (*AddResult, error)
Add resolves a package's latest-satisfying version, inserts it into the manifest at its alphabetic position, and runs Sync.
func (*Client) Outdated ¶
func (c *Client) Outdated(ctx context.Context, opts OutdatedOptions) ([]OutdatedReport, error)
Outdated reports each lockfile entry's status against the registry's current state.
func (*Client) RegisterResolver ¶
RegisterResolver attaches a resolver for the given purl type. Overwrites any previously-registered resolver. Resolvers are read-only after registration; operations dispatch on resolved purl type at sync time.
func (*Client) Remove ¶
func (c *Client) Remove(ctx context.Context, names []string, opts SyncOptions) (*SyncResult, error)
Remove deletes the named entries from the manifest and runs Sync to clean up the resulting lockfile and on-disk files.
func (*Client) Resolver ¶
Resolver returns the resolver registered for the given purl type, or nil. Useful for custom resolvers that delegate to a built-in for non-matching purls.
func (*Client) Sync ¶
func (c *Client) Sync(ctx context.Context, opts SyncOptions) (*SyncResult, error)
Sync resolves the manifest, fetches assets, and writes the lockfile. Per-operation behaviour comes from opts; infrastructure config (RegistryURL, SignatureMode, ...) comes from the Client.
func (*Client) Verify ¶
func (c *Client) Verify(opts VerifyOptions) (*VerifyResult, error)
Verify re-hashes every file under the lockfile's OutDir and compares against the recorded integrity. With opts.Strict, npm assets additionally re-derive their per-file integrity by re-fetching the registry tarball.
type ClientOptions ¶
type ClientOptions struct {
// HTTPClient overrides the default, which is the safehttp transport
// (SSRF-safe dial gate, redirect cap, scheme allowlist).
HTTPClient *http.Client
// RegistryURL overrides the default npm registry. Honoured by the
// built-in npm resolver only.
RegistryURL string
Forge forge.Options
// SignatureMode controls npm dist.signatures verification. Zero is
// SignatureModeWarn.
SignatureMode npm.SignatureMode
// Verifier validates each attestation bundle the built-in npm and
// forge resolvers record. Nil means record-only — attestations
// land in the lockfile but the certificate chain and inclusion
// proof are not checked. The pin CLI sets this to sigstore.New(<TUF
// root>) when --verify-provenance is passed.
Verifier source.ProvenanceVerifier
}
ClientOptions configures a Client. Zero values give the CLI defaults.
type ListEntry ¶
type ListEntry struct {
Name string `json:"name"`
Version string `json:"version"`
PURL string `json:"purl"`
Path string `json:"path"`
Out string `json:"out"`
Type string `json:"type"`
Integrity string `json:"integrity"`
Size int64 `json:"size"`
}
func List ¶
func List(opts VerifyOptions) ([]ListEntry, error)
List returns one ListEntry per vendored file recorded in the lockfile.
type OutdatedOptions ¶
type OutdatedReport ¶
type OutdatedReport struct {
Name string
Locked string
Latest string
Behind bool
AgeDays int
LastPublish string
Deprecated string
Yanked bool
ProvenanceDowngrade bool // locked had provenance, latest doesn't
ProvenanceUpgrade bool // locked didn't, latest does
// LicenseLocked / LicenseLatest are SPDX-normalised. LicenseChange
// is true when both are non-empty and differ; bumping should
// re-evaluate license compatibility.
LicenseLocked string
LicenseLatest string
LicenseChange bool
// Unmaintained is informational; does not affect Severity or
// OutdatedExitCode.
Unmaintained bool
}
OutdatedReport is one row of pin.Outdated. Severity reports the most-severe finding: ok / behind / deprecated / provenance-downgrade / yanked.
func Outdated ¶
func Outdated(ctx context.Context, opts OutdatedOptions) ([]OutdatedReport, error)
func (*OutdatedReport) Severity ¶
func (r *OutdatedReport) Severity() string
Example ¶
ExampleOutdatedReport_Severity collapses an OutdatedReport into a single label. Useful when filtering or grouping reports in a UI.
package main
import (
"fmt"
pin "github.com/git-pkgs/pin"
)
func main() {
reports := []pin.OutdatedReport{
{Name: "fine", Behind: false},
{Name: "stale", Behind: true},
{Name: "dropped", Yanked: true},
}
for _, r := range reports {
fmt.Printf("%s: %s\n", r.Name, r.Severity())
}
}
Output: fine: ok stale: behind dropped: yanked
type SBOMFormat ¶
type SBOMFormat string
const ( SBOMCycloneDXJSON SBOMFormat = "cyclonedx" SBOMCycloneDXXML SBOMFormat = "cyclonedx-xml" SBOMSPDXJSON SBOMFormat = "spdx" )
type SBOMOptions ¶
type SBOMOptions struct {
Dir string
Lock string
Format SBOMFormat
StripPinProperties bool
}
SBOMOptions. Format defaults to CycloneDX (the lockfile's native shape, byte-for-byte passthrough when StripPinProperties is false). StripPinProperties drops every property whose name starts with "pin:" before encoding; the lockfile on disk is untouched.
type SyncOptions ¶
type SyncOptions struct {
Dir string
Manifest string
Lock string
DryRun bool
Frozen bool
RegistryURL string
Forge forge.Options
// Update lists entry names whose lock-is-sticky check is
// bypassed; UpdateAll bypasses it for every entry.
Update []string
UpdateAll bool
// StrictProvenance fails sync when an npm entry resolves to a
// version with no SLSA Provenance attestation recorded.
StrictProvenance bool
// RequirePublisherMatchesRepository fails sync when an
// attestation's build workflow lives on a different repository
// than the package's declared repository.url. The consumer-side
// check against leaked-token attacks: a stolen publish token
// produces an attestation whose source_repository won't match.
RequirePublisherMatchesRepository bool
// VerifyProvenance cryptographically verifies each attestation
// bundle against Sigstore's TUF trust root.
VerifyProvenance bool
SignatureMode npm.SignatureMode
// Concurrency caps parallel resolves; zero = defaultConcurrency.
// Lockfile order is independent of completion order: assets are
// sorted by (name, asset.out) before writing.
Concurrency int
// NoFetch implies Frozen and re-hashes every vendored file on
// disk against the lockfile's recorded integrity. For CI jobs
// that vendored at image-build time. No network, no writes.
NoFetch bool
// FS redirects Sync's outputs (vendored files + pin.lock). nil
// means pinfs.OS(opts.Dir). pinfs.NewMemory() keeps everything in
// process. The manifest and prior lockfile are still read from
// opts.Dir on local disk.
FS pinfs.Writer
}
SyncOptions configures pin.Sync / Client.Sync. RegistryURL, Forge, SignatureMode, and VerifyProvenance are honoured by the top-level pin.Sync shim only; pass them via ClientOptions when constructing a Client directly.
type SyncResult ¶
func Remove ¶
func Remove(ctx context.Context, names []string, opts SyncOptions) (*SyncResult, error)
func Sync ¶
func Sync(ctx context.Context, opts SyncOptions) (*SyncResult, error)
Sync constructs a Client from opts and runs one Sync. Consumers that reuse a Client across calls should use New + Client.Sync.
Example ¶
ExampleSync resolves a manifest and writes the lockfile + vendored files into a project directory. A real consumer would point RegistryURL at registry.npmjs.org; this example spins up a tiny in-memory registry so it can run hermetically.
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
pin "github.com/git-pkgs/pin"
)
func main() {
srv := exampleRegistry("demo", "1.0.0", map[string]string{
"dist/demo.js": "console.log('demo')",
})
defer srv.Close()
dir, err := os.MkdirTemp("", "pin-example-sync-")
if err != nil {
panic(err)
}
defer func() { _ = os.RemoveAll(dir) }()
manifest := `out: "vendor"
assets:
- name: "demo"
version: "1.0.0"
files: ["dist/demo.js"]
`
if err := os.WriteFile(filepath.Join(dir, pin.DefaultManifest), []byte(manifest), 0o644); err != nil {
panic(err)
}
res, err := pin.Sync(context.Background(), pin.SyncOptions{
Dir: dir,
RegistryURL: srv.URL,
})
if err != nil {
panic(err)
}
fmt.Printf("synced %d asset\n", len(res.Lock.Assets))
}
// exampleRegistry stands up an httptest.Server serving one
// npm-shaped package version: a single .tgz with one file inside and
// the matching version JSON. Kept in this file so the Examples above
// stay self-contained; not part of pin's public API.
func exampleRegistry(name, version string, files map[string]string) *httptest.Server {
pj, _ := json.Marshal(map[string]any{"name": name, "version": version})
all := map[string]string{"package.json": string(pj)}
maps.Copy(all, files)
var tarBuf bytes.Buffer
gz := gzip.NewWriter(&tarBuf)
tw := tar.NewWriter(gz)
for p, c := range all {
_ = tw.WriteHeader(&tar.Header{Name: "package/" + p, Mode: 0o644, Size: int64(len(c))})
_, _ = tw.Write([]byte(c))
}
_ = tw.Close()
_ = gz.Close()
tarball := tarBuf.Bytes()
h := sha512.Sum512(tarball)
integrity := "sha512-" + base64.StdEncoding.EncodeToString(h[:])
mux := http.NewServeMux()
var url string
mux.HandleFunc("/"+name+"/"+version, func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"name": name,
"version": version,
"license": "MIT",
"dist": map[string]any{"tarball": url + "/tarball.tgz", "integrity": integrity},
})
})
mux.HandleFunc("/tarball.tgz", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(tarball)
})
srv := httptest.NewServer(mux)
url = srv.URL
return srv
}
Output: synced 1 asset
Example (InMemory) ¶
ExampleSync_inMemory pipes pin's outputs into an in-memory writer instead of the local filesystem. Useful for build systems that assemble artefacts in-process, for tests, or for any consumer that wants the resolved bytes without touching disk. The manifest still has to live on disk under SyncOptions.Dir; only the writes (vendored files + pin.lock) are diverted.
package main
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt"
"maps"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
pin "github.com/git-pkgs/pin"
"github.com/git-pkgs/pin/pinfs"
)
func main() {
srv := exampleRegistry("demo", "1.0.0", map[string]string{
"dist/demo.js": "console.log('demo')",
})
defer srv.Close()
dir, err := os.MkdirTemp("", "pin-example-mem-")
if err != nil {
panic(err)
}
defer func() { _ = os.RemoveAll(dir) }()
if err := os.WriteFile(filepath.Join(dir, pin.DefaultManifest), []byte(`out: "v"
assets:
- name: "demo"
version: "1.0.0"
files: ["dist/demo.js"]
`), 0o644); err != nil {
panic(err)
}
mem := pinfs.NewMemory()
if _, err := pin.Sync(context.Background(), pin.SyncOptions{
Dir: dir,
RegistryURL: srv.URL,
FS: mem,
}); err != nil {
panic(err)
}
js, _ := mem.Get("v/demo/demo.js")
fmt.Println(string(js))
_, hasLock := mem.Get("pin.lock")
fmt.Println("lock in memory:", hasLock)
}
// exampleRegistry stands up an httptest.Server serving one
// npm-shaped package version: a single .tgz with one file inside and
// the matching version JSON. Kept in this file so the Examples above
// stay self-contained; not part of pin's public API.
func exampleRegistry(name, version string, files map[string]string) *httptest.Server {
pj, _ := json.Marshal(map[string]any{"name": name, "version": version})
all := map[string]string{"package.json": string(pj)}
maps.Copy(all, files)
var tarBuf bytes.Buffer
gz := gzip.NewWriter(&tarBuf)
tw := tar.NewWriter(gz)
for p, c := range all {
_ = tw.WriteHeader(&tar.Header{Name: "package/" + p, Mode: 0o644, Size: int64(len(c))})
_, _ = tw.Write([]byte(c))
}
_ = tw.Close()
_ = gz.Close()
tarball := tarBuf.Bytes()
h := sha512.Sum512(tarball)
integrity := "sha512-" + base64.StdEncoding.EncodeToString(h[:])
mux := http.NewServeMux()
var url string
mux.HandleFunc("/"+name+"/"+version, func(w http.ResponseWriter, _ *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"name": name,
"version": version,
"license": "MIT",
"dist": map[string]any{"tarball": url + "/tarball.tgz", "integrity": integrity},
})
})
mux.HandleFunc("/tarball.tgz", func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write(tarball)
})
srv := httptest.NewServer(mux)
url = srv.URL
return srv
}
Output: console.log('demo') lock in memory: true
type VerifyOptions ¶
VerifyOptions: Strict turns the cheap on-disk re-hash into a tarball re-derive for npm assets.
type VerifyResult ¶
VerifyResult. Failed reports whether any drift or missing-file was seen; Extra is informational unless opts.Strict.
func Verify ¶
func Verify(opts VerifyOptions) (*VerifyResult, error)
func (*VerifyResult) Failed ¶
func (r *VerifyResult) Failed() bool
func (*VerifyResult) Summary ¶
func (r *VerifyResult) Summary() string
Example ¶
ExampleVerifyResult_Summary prints the one-line summary the CLI renders at the end of `pin verify`. Library consumers driving a UI can reuse the same string.
package main
import (
"fmt"
pin "github.com/git-pkgs/pin"
)
func main() {
r := &pin.VerifyResult{
OK: []string{"a", "b", "c"},
Missing: []string{"d"},
Drifted: []pin.Drift{{Out: "e"}},
}
fmt.Println(r.Summary())
}
Output: 3 ok, 1 missing, 1 drifted
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package assets is the runtime helper a Go web app uses to consume pin's output: parse the lockfile, serve the vendored files, and emit HTML tags with integrity attributes.
|
Package assets is the runtime helper a Go web app uses to consume pin's output: parse the lockfile, serve the vendored files, and emit HTML tags with integrity attributes. |
|
Package cdn builds URLs for npm package files served via public CDNs.
|
Package cdn builds URLs for npm package files served via public CDNs. |
|
cmd
|
|
|
pin
command
|
|
|
Package integrity provides Subresource Integrity helpers.
|
Package integrity provides Subresource Integrity helpers. |
|
internal
|
|
|
Package lock reads and writes pin.lock as a CycloneDX 1.6 BOM.
|
Package lock reads and writes pin.lock as a CycloneDX 1.6 BOM. |
|
Package manifest reads and validates pin.yaml.
|
Package manifest reads and validates pin.yaml. |
|
Package pinfs is the writable-filesystem abstraction for pin's outputs: vendored asset files and the pin.lock.
|
Package pinfs is the writable-filesystem abstraction for pin's outputs: vendored asset files and the pin.lock. |
|
scripts
|
|
|
generate-docs
command
Regenerates a flat tree of Markdown reference pages — one per pin subcommand — under ./docs/reference/.
|
Regenerates a flat tree of Markdown reference pages — one per pin subcommand — under ./docs/reference/. |
|
generate-man
command
Regenerates the troff man pages for every pin subcommand under ./man/.
|
Regenerates the troff man pages for every pin subcommand under ./man/. |
|
Package sniff detects the module format of a JavaScript file from its bytes.
|
Package sniff detects the module format of a JavaScript file from its bytes. |
|
Package source is the plug-in surface for new source kinds.
|
Package source is the plug-in surface for new source kinds. |
|
forge
Package forge resolves manifest entries against git forges.
|
Package forge resolves manifest entries against git forges. |
|
npm
Package npm resolves manifest entries against the npm registry, anchoring per-file integrity to the registry-published tarball hash.
|
Package npm resolves manifest entries against the npm registry, anchoring per-file integrity to the registry-published tarball hash. |
|
rawurl
Package rawurl implements source.Resolver for url: manifest sources.
|
Package rawurl implements source.Resolver for url: manifest sources. |