pin

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 34 Imported by: 0

README

pin

pin vendors browser assets without npm: a single static binary that fetches files from published packages, anchors their integrity to the registry tarball, commits them to your repo, and writes a lockfile that is also a valid CycloneDX SBOM.

If your server-rendered app needs htmx, a CSS kit, and an icon set, that's three dependencies. Running npm install for them gives you a node_modules with hundreds of transitive packages, a lockfile format you don't otherwise use, a Node runtime in CI, and arbitrary code execution on every install via lifecycle hooks. pin fetches the files you name at the versions you pin, hashes them against what npm published, and writes them to disk without running install scripts, hooks, or plugin loaders.

Install

Homebrew:

brew tap git-pkgs/git-pkgs
brew install pin

Go:

go install github.com/git-pkgs/pin/cmd/pin@latest

Or grab a binary from the releases page once tagged.

Quickstart

Write pin.yaml:

out: "internal/web/static/vendor"

assets:
  - name: "htmx.org"
    version: "^2.0"
    files: ["dist/htmx.min.js"]

  - name: "@tailwindcss/browser"
    version: "4.1.13"

  - name: "lucide"
    version: "^0.545"
    files: ["dist/umd/lucide.min.js"]

  - name: "highlight.js"
    version: "11.11.1"
    source: "github:highlightjs/cdn-release"
    files:
      - "build/highlight.min.js"
      - "build/styles/github.min.css"

Run pin sync to get:

internal/web/static/vendor/
  htmx.org/htmx.min.js
  tailwindcss__browser/index.global.js
  lucide/lucide.min.js
  highlightjs__cdn-release/highlight.min.js
  highlightjs__cdn-release/github.min.css
pin.lock

The version field accepts exact pins (2.0.6), semver ranges (^2.0, ~0.3.11), or npm dist-tags (latest, next). Once a version is locked, it stays locked: pin sync re-uses the locked version as long as the manifest constraint still allows it, and pin update bumps within a range. When files: is omitted for an npm source, pin reads the package's package.json and picks the entry point from jsdelivr || unpkg || browser || module || main.

Source kinds

- name: "htmx.org"                       # npm (default)
  version: "^2.0"

- name: "highlight.js"                   # GitHub release
  version: "11.11.1"
  source: "github:highlightjs/cdn-release"
  files: ["build/highlight.min.js"]

- name: "my-asset"                       # Raw URL (TOFU)
  version: "1.0.0"
  source: "url:https://example.com/dist/asset.js"

github: sources resolve the tag to a commit SHA, fetch via jsdelivr's /gh/ mirror, and record the SHA in the lockfile as the integrity anchor. url: sources hash the bytes on first fetch and verify against the recorded hash on every subsequent sync. Both go through the same source.Resolver interface, so adding gitlab/codeberg/bitbucket later is a single new file.

--registry (or SyncOptions.RegistryURL) overrides the npm registry for the whole sync. For a single entry, add registry_url: to that asset:

assets:
  - name: "private-pkg"
    version: "1.0.0"
    files: ["dist/x.js"]
    registry_url: "https://npm.private.example/"

pin records the override on the asset's purl as a repository_url qualifier in pin.lock, so the lockfile round-trips faithfully. ~/.npmrc and registry-auth tokens are not read; private registries pin reaches today are ones that don't require credentials or whose credentials live in the URL.

Commands

pin sync                       resolve manifest, fetch assets, write lockfile (alias: pin install)
pin sync --frozen              fail before any network if manifest and lockfile disagree (CI)
pin sync --no-fetch            --frozen plus re-hash on-disk files against the lockfile; no network, no writes
pin sync --concurrency=N       cap parallel resolves (default 8)
pin sync --dry-run [--json]    resolve and report, write nothing
pin update [NAME...]           re-resolve to highest satisfying version, ignoring the lock
pin verify [--strict] [--json] re-hash files on disk against the lockfile (exit 4 on drift)
pin outdated [--json]          compare locked versions against the registry's latest
pin add NAME[@SPEC] [FILE...]  append to the manifest at alphabetic position and sync
pin rm NAME...                 remove entries from the manifest and sync
pin list [--json]              print the lockfile contents
pin path NAME                  print the on-disk paths for a locked package
pin init                       write a starter pin.yaml in the current directory
pin sbom [-f spdx|cyclonedx-xml] [-o FILE]  emit the lockfile as an SBOM

pin sync prints a one-line stderr nudge when it detects a CI environment (CI, GITHUB_ACTIONS, GITLAB_CI, BUILDKITE, CIRCLECI, JENKINS_URL) and --frozen is not set.

Safe defaults

The cooldown window (min_release_age) is on by default at 48 hours. Most malicious npm versions are caught within 24 to 48 hours, and the window blocks the majority of fresh-publish supply-chain attacks. Ranges fall back to the next-highest satisfying version outside the window; dist-tags fail with a clear error if latest is too fresh; exact pins bypass the window because you named the version explicitly. Opt out with min_release_age: 0 at the manifest top level or per entry.

--frozen is the CI safety flag: it bails before any network if the manifest and lockfile disagree. --no-fetch adds a re-hash of every vendored file against the lockfile's recorded integrity on top of --frozen, for CI jobs that vendored at image-build time and want to assert nothing was tampered with after git checkout without doing any network or any writes.

pin sync rewrites the lockfile only when the manifest changed; identical bytes skip the write. pin runs no code from a fetched package, which puts stages 5 and 6 of The Stages of Package Installation out of scope.

Provenance and trusted publishing

For npm and GitHub forge sources, when the publisher used trusted publishing, pin sync records the SLSA Provenance v1 attestation in the lockfile: builder_id (the CI workflow URI), source_repository, source_revision, signer_identity (the OIDC SAN), and the bundle URL.

Three opt-in flags layer the trust assertion:

pin sync --strict-provenance
   fail if any entry resolves to a version with no attestation.

pin sync --require-publisher-matches-repository
   fail if an attestation's source repository differs from the package's declared
   repository.url. Catches leaked-token attacks: a stolen publish token can sign
   a valid bundle from the attacker's CI, but the source_repository field then
   won't match the legitimate package's repo.

pin sync --verify-provenance
   cryptographically verify the sigstore bundle against the live Sigstore TUF
   trust root: Fulcio cert chain, Rekor inclusion proof, DSSE signature,
   subject digest matches the fetched artifact. Composes with the other two.
   Trust root is cached at $XDG_CACHE_HOME/pin/sigstore-tuf/ after first use.

pin sync --signature-mode {warn|enforce|off}
   verify npm dist.signatures (ECDSA P-256 over {name}@{version}:{integrity},
   keys fetched from /-/npm/v1/keys). warn (default) fails on bad sigs but
   tolerates absent ones; enforce additionally fails on absent.

The persistent form of these per-invocation flags is a manifest trust: block, set top-level or per-entry:

trust:
  require_provenance: true
  require_publisher_matches_repository: true
  trusted_workflows:
    - https://github.com/builder-org/builder/.github/workflows/release.yml

assets:
  - name: monorepo-pkg
    version: ^1.0.0
    trust:
      require_publisher_matches_repository: false   # entry-level override

trusted_workflows is the escape hatch for monorepo packages whose legitimate build workflow lives on a different repo than the package's declared repository.url. CLI flags always win over manifest entries: --strict-provenance forces the check even on an entry that opted out.

pin outdated flags a provenance-downgrade severity (above deprecated, below yanked) when the locked version had an attestation and the latest doesn't, which surfaces the case where the maintainer (or whoever now controls the publish token) disabled trusted publishing.

Lockfile

pin.lock is a valid CycloneDX 1.6 SBOM. Each package becomes a library component with the registry tarball hash; each vendored file becomes a nested file component with its own SHA-384, the CDN URL, and pin-specific metadata under a pin: property namespace. Any CycloneDX consumer (Dependency-Track, GUAC, OSV-scanner, git-pkgs sbom) reads it directly. serialNumber and metadata.timestamp are deliberately omitted so re-runs are byte-stable and parallel branches don't conflict on the file.

The schema is in docs/SPEC.md, the defences in docs/SECURITY.md, and the adversary-by-asset model in docs/THREAT_MODEL.md.

Integrity

On first sync of an npm package version, pin fetches the registry metadata, downloads the published tarball, verifies it against npm's dist.integrity, extracts the requested files, and computes a SHA-384 over each one. Subsequent syncs of the same version verify against the recorded hash, so the CDN URLs in the lockfile are a transport hint rather than the integrity anchor.

GitHub sources anchor on the commit SHA (recorded as a SHA-1 hash on the library component plus a vcs_revision qualifier on the purl); url sources anchor on the per-file SHA-384, established Trust-On-First-Use.

Format sniffing

For each vendored script, pin detects the module format (esm, umd, iife, cjs, amd, system, or unknown) by scanning the bytes with a comment- and string-aware regex pass. The result lands in the lockfile's pin:format property so importmap consumers can filter to ESM entries. Override per-entry with format: in the manifest.

What doesn't work

pin is for self-contained distributables: UMD bundles, IIFE builds, ESM modules with no bare-specifier imports, CSS files. It does not work for packages that expect a module graph at runtime, and it does not run install scripts. If a package's real payload arrives via a postinstall hook (a platform binary downloaded after the tarball lands) pin will vendor only the stub. Point files: at the package's pre-bundled CDN distribution if it ships one; if it doesn't ship one and depends on a bundler or postinstall to assemble itself, it's out of scope.

As a Go library

For one-shot scripts, the package-level functions take the same options the CLI flags wrap (the CLI is itself a thin shim over them):

import "github.com/git-pkgs/pin"

res, err := pin.Sync(ctx, pin.SyncOptions{Dir: "."})

For long-lived processes (a Rails gem, a CI service, a custom integrator) the pin.Client pattern lets one instance reuse its HTTP connection pool and source resolvers across calls:

c := pin.New(pin.ClientOptions{RegistryURL: "https://registry.npmjs.org"})

c.Sync(ctx, pin.SyncOptions{Dir: "./app-a"})
c.Sync(ctx, pin.SyncOptions{Dir: "./app-b"})
c.Verify(pin.VerifyOptions{Dir: "./app-a"})

Source resolvers are pluggable by purl type. Register a new resolver for any prefix (pkg:ipfs/..., an internal artifact registry, etc.) and Sync will dispatch manifest entries with that purl to it:

c.RegisterResolver("ipfs", myIPFSResolver{})

The full Client surface: Sync, Verify, Outdated, Add, Remove, plus the package-level List, Path, Init, SBOM, EncodeLock. The manifest, lock, pinfs, integrity, cdn, sniff, source (with source/npm, source/forge, source/rawurl), and assets sub-packages are all public.

SyncOptions.FS redirects pin's outputs (vendored files + pin.lock) into anything that implements pinfs.Writer. The default writes to local paths under SyncOptions.Dir; pinfs.NewMemory() keeps everything in process, and a custom implementation can pipe writes into a tarball, an archive, or an in-memory build artefact.

Provenance handling lives in two sibling modules: github.com/git-pkgs/attestation (stdlib-only SLSA Provenance v1 bundle parser) and github.com/git-pkgs/sigstore (sigstore-go wrapper that verifies any (digestAlg, digest) pair against the Sigstore TUF trust root). Both can be imported independently of pin.

The assets package is the runtime helper a Go web app uses to consume pin's output: parse the lockfile, serve the vendored files via fs.FS, and emit HTML tags with integrity and crossorigin attributes from a template.

Failure modes surface as wrapped sentinel errors: errors.Is(err, pin.ErrFrozenDrift), pin.ErrVerifyFailed, pin.ErrProvenanceMissing, pin.ErrPublisherMismatch, pin.ErrPathEscape, pin.ErrPathCollision, pin.ErrNoLockfile.

Framework integration

The assets package imports only lock and the standard library, so any Go web framework that takes an fs.FS (or a directory) and any template engine that accepts template.HTML works without a framework-specific adapter.

Framework Serve Tag emission
net/http http.FileServer(http.FS(afs)) assets.Tag / Tags in html/template
Chi r.Handle("/vendor/*", http.FileServer(...)) same
Gin r.StaticFS("/vendor", http.FS(afs)) template helper that returns template.HTML
Echo e.StaticFS("/vendor", afs) renderer that accepts template.HTML
Fiber app.Use("/vendor", filesystem.New(...)) engine-specific Raw helper
Templ http.FileServer(http.FS(afs)) @templ.Raw(assets.Tag(lock, name, opts)[0])
Wails bundle alongside the embedded UI inline in the embedded HTML

Common shape regardless of framework:

import (
    "bytes"
    "embed"

    "github.com/git-pkgs/pin/assets"
)

//go:embed static/vendor pin.lock
var vendored embed.FS

lockBytes, _ := vendored.ReadFile("pin.lock")
lock, _ := assets.Parse(bytes.NewReader(lockBytes))
afs, _ := assets.FS(vendored, lock)

// afs implements fs.FS — pass to http.FileServer(http.FS(afs)) or any
// framework's static-file handler. Render tags from your template with
// assets.Tag(lock, "htmx.org", assets.Options{Prefix: "/vendor/"}).

Embedding vendored bytes in the binary

For single-binary distribution, point pin sync at a directory inside your module and //go:embed it alongside the lockfile:

# pin.yaml
out: "internal/web/static/vendor"
//go:embed internal/web/static/vendor pin.lock
var vendored embed.FS

assets.Parse + assets.FS read both from the same embed.FS, so the binary has no runtime filesystem dependency and no separate static/vendor directory to ship. pin verify --no-fetch runs against the on-disk copy before the build to confirm the embedded bytes are what the lockfile claims.

Stability

The Go API at github.com/git-pkgs/pin covers the functions Sync, Add, Outdated, Verify, Remove, List, Path, Init, SBOM, EncodeLock, and New, plus the Client reusable-client pattern and the option, result, and error types they take. The lock, manifest, pinfs, and assets sub-packages are covered too, along with the sentinel errors. Removing or renaming any of these requires a new major version (/v2, /v3). New fields on option structs are additive and don't bump the major version.

pin.lock carries a pin:lockfile_version property under the CycloneDX metadata. A binary refuses any lockfile whose version it doesn't recognise. New fields land as additive properties under the pin: namespace and don't bump the version. A version bump only happens on incompatible schema changes and arrives as a separate release with migration notes.

pin.yaml follows the same convention: new fields are additive, existing fields keep their meaning across releases, and removals or semantic changes ship under a new major version with explicit migration steps.

The pinfs.Writer interface and the source.Resolver interface are stable in shape: adding a method to either is a breaking change. New behaviour goes on a parallel interface or option struct instead.

api_stability_test.go references every public symbol so a removed or renamed export breaks go build immediately; pkg.go.dev is the live record.

License

MIT

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

Examples

Constants

View Source
const (
	SeverityOK                  = "ok"
	SeverityBehind              = "behind"
	SeverityDeprecated          = "deprecated"
	SeverityYanked              = "yanked"
	SeverityProvenanceDowngrade = "provenance-downgrade"
)
View Source
const (
	ExitOutdated = 7
	ExitYanked   = 9
)
View Source
const (
	DefaultManifest = "pin.yaml"
	DefaultLock     = "pin.lock"

	ToolName = "pin"
)

Variables

View Source
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
View Source
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

func EncodeLock(l *lock.Lock) ([]byte, error)

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

func Init(dir, manifestPath string) error

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

func Path

func Path(name string, opts VerifyOptions) ([]string, error)

Path returns the on-disk paths for every vendored file belonging to the named package.

func SBOM

func SBOM(w io.Writer, opts SBOMOptions) error

SBOM writes the lockfile in the requested SBOM format. CycloneDX JSON is byte-for-byte when no filtering is requested; other formats (and any strip) round-trip through git-pkgs/sbom.

Types

type AddOptions

type AddOptions struct {
	Dir         string
	Manifest    string
	Lock        string
	RegistryURL string
	Exact       bool
	DryRun      bool
}

type AddResult

type AddResult struct {
	Entry      manifest.Entry
	Resolved   string
	SyncResult *SyncResult
}

AddResult has SyncResult == nil under --dry-run.

func Add

func Add(ctx context.Context, spec string, files []string, opts AddOptions) (*AddResult, error)

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

func (c *Client) RegisterResolver(purlType string, r source.Resolver)

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

func (c *Client) Resolver(purlType string) source.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 Drift

type Drift struct {
	Out      string
	Expected string
	Actual   string
}

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 OutdatedOptions struct {
	Dir         string
	Lock        string
	RegistryURL string
}

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

type SyncResult struct {
	Lock    *lock.Lock
	Changes lock.Changes
	Written []string
	Removed []string
}

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

type VerifyOptions struct {
	Dir         string
	Lock        string
	Strict      bool
	RegistryURL string
}

VerifyOptions: Strict turns the cheap on-disk re-hash into a tarball re-derive for npm assets.

type VerifyResult

type VerifyResult struct {
	OK      []string
	Missing []string
	Drifted []Drift
	Extra   []string
}

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

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
cli
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL