Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

v0 source pure

Package gnopm is the domain model of a source registry for gno packages: a map from a deployed package path back to t...

Readme View source

gno.land/p/moul/gnopm/v0

The domain model of a source registry for gno packages: a map from a deployed package path back to the repository, commit and directory that produced it.

The realm half is gno.land/r/moul/gnopm/registry/v0. This package is pure: no chain imports, no realm state, and the one piece of chain knowledge it needs (who holds a namespace) arrives as a function parameter.

Why this has to exist at all

A Go import path is a repository URL, so pkg.go.dev needs no registry. A gno import path is a chain address, so nothing anywhere says which source produced the bytes running at gno.land/p/nt/tinyavl/v0. gno gave up that link deliberately, and this is the part that has to be rebuilt by hand.

What it can and cannot promise

It cannot promise anything, and the whole design is built around saying so rather than around hiding it. Two claims look alike and are not the same:

claim provable
this package came from that repository no. Not here and not anywhere: anyone may deploy any bytes and claim any repository
the deployed bytes equal the addpkg payload of that directory at that commit yes, by hashing both sides. Off chain, by anything that can clone
the claimant owns the path's namespace yes, from chain data, recomputed on every read

So a Claim is testimony under a signature, and it is stored in exactly the shape the second row needs: path, repository, commit and directory are the four inputs to "hash the payload of that directory and compare it with what the chain hands back". This package does not answer the question. It makes the question answerable by something that can clone, which gnopm already does against a local tree (gnopm verify -deployed).

A mismatch, when a verifier does find one, is not evidence of malice. The likeliest cause by far is a repository that moved on after a deploy, which is the normal state of most repositories most of the time.

Open, and tagged

Anyone may claim any path, including one they had nothing to do with. Claim.OwnedBy reports whether a claim comes from the party that controls the path, so the two can be told apart at render time.

Gating registration on namespace ownership was the alternative and it cannot bootstrap: on day one almost nothing has been registered by its own deployer, so the gated registry is empty and teaches nobody anything. Open-and-labelled keeps the map fillable and moves the defence to the renderer, which is why OwnedBy exists and why the realm ranks on it rather than hiding anything.

OwnedBy takes the name resolver as a parameter and the answer is never stored: a name can be transferred, and a stored answer would rot into exactly the kind of stale claim this package exists to distinguish from a live one.

API

 1r := gnopm.New()
 2
 3// A claimant states where a package came from. Calling it again replaces that
 4// claimant's own claim and nobody else's.
 5c, err := r.Register(claimant, height, pkgPath, repo, commit, dir, ref)
 6err = r.Withdraw(claimant, pkgPath)          // your own claim, never anyone else's
 7
 8p := r.Package(pkgPath)                      // every claim about one path, or nil
 9p.Claim(claimant)                            // one of them
10p.IterateClaims(func(c *gnopm.Claim) bool { ... })
11
12c.OwnedBy(resolve)                           // does this come from the namespace holder
13c.SourceURL()                                // a browsable link to the claimed source

Height dates the claim and UpdatedAt dates the last edit. They are separate so a reader can tell a claim that has been kept current from one made once at deploy time and abandoned.

Validation is an allowlist, on purpose

Every stored field is charset-validated at write time: ValidPkgPath, ValidRepo, ValidCommit, ValidDir, ValidRef. Each is an allowlist, so a stored field cannot hold a backtick, a pipe, a bracket, an ASCII control character or a bidi override.

That is not belt-and-braces, it is the escaping strategy. One claim is rendered in several places (a listing row, a table cell, an inline-code span, a link title) and a consumer that forgets to escape at any one of them has a hole. A field that can only hold safe bytes needs no escaping anywhere.

Two consequences worth knowing before they surprise you:

  • ValidRepo accepts https:// and nothing else. Not a taste judgement about git transports: the set of URL schemes that are safe to hand a browser is exactly one, and javascript: and data: are the attack. A host with a port is also refused, because the userinfo check (https://[email protected]/x fetches from evil.example while reading as GitHub) needs the host to contain no @ or :.
  • ValidRef is narrower than git's own rule. git rejects a handful of metacharacters and permits the rest, which would leave a ref free to carry a pipe or a backtick and undo the allowlist for every other field. What stays expressible is every ref anyone actually has.

What is deliberately not here

  • No verification. A realm cannot clone a repository. See the realm README for where that half is meant to live.
  • No network of trust, no voting, no curation. Whose claim to believe is a judgement, and the place to make it is a renderer that can see who deployed the package, not a data structure.
  • No version resolution. Version reads a trailing vN and that is all. An unversioned path is legal and is not an error: gnoweb serves gno.land/u/<name> by calling the realm at exactly /r/<name>/home, so that one path can never carry a version.

Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.

Dependency graph:

gno.land/p/moul/gnopm/v0 dependency graph

⚠️ Disclaimer: provided as-is, without warranty; not security-audited. Full disclaimer: DISCLAIMER.

Overview

Package gnopm is the domain model of a source registry for gno packages: a map from a deployed package path back to the repository, commit and directory that produced it.

It exists because gno deliberately broke the link Go gets for free. A Go import path IS a repository URL, so pkg.go.dev needs no registry; a gno import path is a chain address, so nothing anywhere says which source produced the bytes running at gno.land/p/nt/tinyavl/v0.

What this can and cannot promise

It cannot promise anything, and the design is built around saying so rather than around hiding it. Two claims look alike and are not the same:

  1. "this package came from that repository". UNPROVABLE, here or anywhere. Anyone may deploy any bytes and anyone may claim any repository. No registry design fixes this, and one that implies otherwise is worse than none.
  2. "the bytes live at path P equal the addpkg payload of directory D in repository R at commit C". Mechanically provable, by hashing both sides. It is NOT provable here: a realm cannot clone a repository. It is provable off chain by anything that can, and gnopm already does exactly this against a local tree (`gnopm verify -deployed`).

So this package stores (1), labelled as the claim it is, in the form (2) needs to be checkable. A Claim is testimony under a signature. It becomes evidence only once something outside the chain has gone and looked.

What a signature does buy

The chain knows who signed a claim, and that is not nothing. A claim whose claimant owns the package path's namespace comes from the party that controls the path, which is the closest thing to a promise available without a chain-level feature. OwnedBy reports that relationship; it is deliberately computed on demand from a caller-supplied resolver and never stored, because a name can be transferred and a stored answer would be a claim of its own.

Anyone may still claim any path. That is the "open but tagged" choice, and it is made on purpose: gating registration on namespace ownership would mean that on day one, when almost nothing is registered by its own deployer, the registry is empty and teaches nobody anything. The defence against a misleading claim is that it renders as a stranger's claim, ranked below, and never as a fact.

Constants 1

const MaxPkgPathLen, MaxRepoLen, MaxDirLen, MaxRefLen, MaxClaimants

 1const (
 2	MaxPkgPathLen = 256
 3	MaxRepoLen    = 512
 4	MaxDirLen     = 256
 5	MaxRefLen     = 255 // git's own limit for a single ref name
 6
 7	// MaxClaimants bounds how many addresses may claim one package path.
 8	// Unbounded, it is a spam surface: one path with ten thousand claims is a
 9	// Render that never returns and a listing nobody can read.
10	MaxClaimants = 16
11)
source

Size caps. Every string the chain stores is bounded: an unbounded field is an unbounded storage deposit, and on gno.land the deposit is paid per byte by whoever writes it.

Variables 1

var ErrInvalidPkgPath, ErrInvalidRepo, ErrInvalidCommit, ErrInvalidDir, ErrInvalidRef, ErrClaimNotFound, ErrTooManyClaimants

1var (
2	ErrInvalidPkgPath   = errors.New("gnopm: invalid package path")
3	ErrInvalidRepo      = errors.New("gnopm: invalid repository URL")
4	ErrInvalidCommit    = errors.New("gnopm: invalid commit id")
5	ErrInvalidDir       = errors.New("gnopm: invalid directory")
6	ErrInvalidRef       = errors.New("gnopm: invalid ref name")
7	ErrClaimNotFound    = errors.New("gnopm: no such claim")
8	ErrTooManyClaimants = errors.New("gnopm: too many claimants for this package")
9)
source

Stable, machine-readable error values. Callers (realms, clients, indexers) switch on these rather than on message text: a realm turns them into panics, and the panic string is the only thing a user ever sees.

Functions 10

func AddressNamespace

1func AddressNamespace(ns string) bool
source

AddressNamespace reports whether ns is shaped like a gno bech32 address, the namespace every account owns without registering anything. A caller still has to check it is the claimant's own address; this only says which of the two ownership rules applies.

func Namespace

1func Namespace(s string) string
source

Namespace returns the owning namespace of a package path, or "" if the path is not one.

func SplitPkgPath

1func SplitPkgPath(s string) (domain, kind, ns, rest string, ok bool)
source

SplitPkgPath breaks a package path into its domain, kind ("p" or "r"), namespace and the remainder. ok is false for anything that is not shaped like a publishable gno path.

func ValidCommit

1func ValidCommit(s string) bool
source

ValidCommit reports whether s is a git object id: 40 (SHA-1) or 64 (SHA-256) lowercase hex characters. Case is fixed so the same object always produces the same stored bytes, and so a verifier comparing two claims compares two comparable strings.

func ValidDir

1func ValidDir(s string) bool
source

ValidDir reports whether s names the subdirectory of the repository holding the package. Empty means the repository root, which is the common case for a single-package repo and must stay expressible.

func ValidPkgPath

1func ValidPkgPath(s string) bool
source

ValidPkgPath reports whether s is shaped like a publishable gno package path.

It does NOT say the path exists on any chain, and cannot: a realm has no way to ask whether some other path was ever deployed. That gap is the registry's central honesty problem and is documented on Registry.

func ValidRef

1func ValidRef(s string) bool
source

ValidRef reports whether s is a fully-qualified git ref, or empty.

The ref is optional and is never the thing that is verified: a ref moves, a commit does not. It is recorded so a reader can tell a claim pinned to a released tag from one pinned to a commit on nobody's branch, and so a verifier can check the claimed commit is still reachable from it rather than dangling behind a force-push.

func ValidRepo

1func ValidRepo(s string) bool
source

ValidRepo reports whether s is a repository URL this registry accepts.

Deliberately narrow: "https://" only, a host, and a path. Not a taste judgement about git transports, a safety one. Whatever is stored here is eventually rendered as a link by this realm, by gnoweb and by every explorer that reads the registry, and the set of schemes that are safe to hand a browser is exactly one. "javascript:", "data:" and "file:" are the attack; "git://" and "ssh://" are merely unreachable from a web page, and a claimant who needs one can point at the https mirror every forge already serves.

func Version

1func Version(s string) string
source

Version returns the trailing "vN" element of a package path, or "" when the path carries none. Absent is legal and is not an error: gnoweb serves gno.land/u/<name> by calling the realm at exactly /r/<name>/home, so that one path can never carry a version.

func New

1func New() *Registry
source

New returns an empty registry.

Types 3

type Claim

struct
 1type Claim struct {
 2	PkgPath  string
 3	Repo     string // https:// URL of the repository
 4	Commit   string // 40 or 64 lowercase hex
 5	Dir      string // subdirectory holding the package, "" for the repo root
 6	Ref      string // fully-qualified ref the commit was on, optional
 7	Claimant address
 8	Height   int64 // block height of the first claim by this claimant
 9	// UpdatedAt is the height of the most recent write. Equal to Height until
10	// the claimant re-registers, which is how a reader tells a claim that has
11	// been kept current from one made once and abandoned.
12	UpdatedAt int64
13}
source

Claim is one address's statement about where a package came from.

Every field except Claimant, Height and UpdatedAt is attacker-controlled. They are charset-validated at write time rather than escaped at read time, so that a consumer which forgets to escape is still safe: the fields cannot hold a byte that means anything to markdown, to a terminal or to a URL parser.

Methods on Claim

func OwnedBy

method on Claim
1func (c *Claim) OwnedBy(resolve func(name string) (address, bool)) bool
source

OwnedBy reports whether c comes from the party that controls the package path's namespace: either the address whose own namespace it is, or whoever resolve says holds the registered name.

resolve maps a namespace name to the address holding it and whether that name is held at all. It is a parameter rather than an import because this package stays pure: the realm supplies r/sys/users, and a test supplies a table.

This is the one label in the whole design that is derived rather than declared, so it is computed here and never stored. A name can be transferred; a stored answer would age into a lie of exactly the kind this package exists to avoid.

func SourceURL

method on Claim
1func (c *Claim) SourceURL() string
source

SourceURL builds a browsable link to the claimed source: the commit, plus the directory when the package is not at the repository root.

GitHub's "/tree/<commit>/<dir>" layout is also GitLab's, Gitea's, Forgejo's and Codeberg's, so one construction covers every forge this registry is likely to meet. It is a convenience, not a promise the link resolves: the repository may be gone, private or renamed since the claim was made, which is itself something a verifier reports rather than something a realm can know.

type Package

struct
1type Package struct {
2	PkgPath string
3	claims  *avl.Tree // claimant address string -> *Claim
4}
source

Package is every claim made about one package path.

Methods on Package

func Claim

method on Package
1func (p *Package) Claim(claimant address) *Claim
source

Claim returns claimant's claim about this package, or nil.

func Count

method on Package
1func (p *Package) Count() int
source

Count is how many addresses have claimed this package.

func IterateClaims

method on Package
1func (p *Package) IterateClaims(cb func(*Claim) bool)
source

IterateClaims walks the claims in claimant-address order. Order is not significance: a caller that wants the owner's claim first must ask OwnedBy, because the registry has no opinion about which claim is true.

type Registry

struct
1type Registry struct {
2	packages *avl.Tree // package path -> *Package
3	claims   int
4}
source

Registry maps a package path to the claims made about its source.

One path may carry several claims, at most one per claimant. That is not a flaw to be resolved: a fork is a legitimate second answer, and so is a third party filling in the map for a package whose author never will.

Methods on Registry

func Claims

method on Registry
1func (r *Registry) Claims() int
source

Claims is the total number of claims across every path.

func IterateNamespace

method on Registry
1func (r *Registry) IterateNamespace(domain, kind, ns string, cb func(*Package) bool)
source

IterateNamespace walks every claimed path under "<domain>/<kind>/<ns>/".

func IteratePackages

method on Registry
1func (r *Registry) IteratePackages(offset, count int, cb func(*Package) bool)
source

IteratePackages walks paths in lexical order, which groups a namespace's packages together without needing a second index. The callback returns true to STOP, following avl's own convention rather than inverting it here.

func Package

method on Registry
1func (r *Registry) Package(pkgPath string) *Package
source

Package returns every claim about a path, or nil.

func Register

method on Registry
1func (r *Registry) Register(claimant address, height int64, pkgPath, repo, commit, dir, ref string) (*Claim, error)
source

Register records or replaces claimant's claim about pkgPath.

Re-registering the same path overwrites that claimant's own previous claim and nobody else's, so the ordinary "I deployed a new commit" flow is one call with no read first. Height is preserved across an update: it dates the claim, not the edit.

func Size

method on Registry
1func (r *Registry) Size() int
source

Size is the number of package paths carrying at least one claim.

func Withdraw

method on Registry
1func (r *Registry) Withdraw(claimant address, pkgPath string) error
source

Withdraw removes claimant's own claim about pkgPath. A claimant can always take back what they said; nobody can remove anyone else's.

Imports 3

Source Files 10