state

package
v0.0.0 Latest Latest
Warning

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

Go to latest
Published: Jun 25, 2026 License: UNKNOWN not legal advice Imports: 0 Imported by: 0

Documentation

Overview

Package state implements the gnoweb state-explorer feature.

It exposes ?state* URLs: the initial full HTML page (?state, ?state&oid), htmx-driven HTML fragments (?state&frag=node|source), and the unchanged JSON API (?state&json, ?state&oid&json, ?state&tid&json).

Index

Constants

View Source
const (
	ClientErrPackageNotFound  = "package not found"
	ClientErrObjectNotFound   = "object not found"
	ClientErrTimeout          = "RPC node request timeout"
	ClientErrBadRequest       = "bad request"
	ClientErrResponseTooLarge = "RPC node response too large"
)

Sentinel error message fragments. The feature/state package cannot import gno.land/pkg/gnoweb (that would create an import cycle), so it cannot use errors.Is against gnoweb's ErrClient* sentinels. Instead, mapClientError matches on the stable error-message substrings produced by gnoweb's client. Exported so gnoweb pins the pact in a test (see TestStateErrorSentinelPact).

View Source
const (
	MaxStateIDLength    = 256
	MaxFragmentLine     = 1_000_000
	MaxFragmentFileSize = 256 * 1024
	MaxSearchQueryLen   = 128
)
View Source
const (
	ViewModePretty = "pretty"
	ViewModeTree   = "tree"
)

View mode constants for the ?state&view= query param. Used everywhere the Go side compares against the mode to avoid string-literal sprawl. Templates still use raw literals — that's UI presentation, not config.

View Source
const (
	KindPrimitive = "primitive"
	KindStruct    = "struct"
	KindArray     = "array"
	KindSlice     = "slice"
	KindMap       = "map"
	KindPointer   = "pointer"
	KindRef       = "ref"
	KindFunc      = "func"
	KindClosure   = "closure"
	KindType      = "type"
	KindInterface = "interface"
	KindPackage   = "package"
	KindNil       = "nil"
	KindCycle     = "cycle"
	KindTruncated = "truncated"
)

Kind constants enumerate the shapes the walker emits. Kept as untyped strings so html/template's `eq` comparator works against `.Kind`.

View Source
const (
	ShapeLeaf   = "leaf"
	ShapeBranch = "branch"
	ShapeRef    = "ref"
)

Node render-shape constants. Shape() classifies a node into exactly one of these so the pretty-card and tree renderers agree on the branch/ref/leaf distinction instead of each recomputing it.

View Source
const StateViewType = components.StateViewType

StateViewType re-exports the components-side constant so feature/state callers (component.go) don't reach across packages for the View tag. Both names resolve to the same underlying ViewType string.

Variables

View Source
var (
	PageTemplate       = mustParse("renderPage", "templates/page.html", "templates/_nodes.html", "templates/_pagination.html")
	FragNodeTemplate   = mustParse("fragNode", "templates/frag_node.html", "templates/_nodes.html", "templates/page.html", "templates/_pagination.html")
	FragSourceTemplate = mustParse("fragSource", "templates/frag_source.html")
	FragErrorTemplate  = mustParse("fragError", "templates/frag_error.html")
	// SearchFragmentTemplate renders the htmx response for the search
	// input: replaces #state-results' innerHTML and OOB-swaps
	// #state-sidebar. Needs the same template tree as PageTemplate so
	// state/decl, state/nodes, state/pagination all resolve.
	SearchFragmentTemplate = mustParse("searchFragment", "templates/page.html", "templates/_nodes.html", "templates/_pagination.html")
)

Pre-parsed templates for the state feature. mustParse panics at init on a malformed template — misconfiguration surfaces immediately, not on the first request.

_nodes.html (state/nodes, state/node, state/source-details) is parsed into BOTH the page and the node-fragment template sets so an htmx-loaded fragment renders with the exact same recursive markup + .row/--depth CSS as the server-rendered tree.

page.html is also parsed into FragNodeTemplate so the fragment can render the pretty-view fields table (state/decl-children + helpers) — parse-time only, no runtime cost beyond extra defines in memory.

View Source
var (
	OIDPattern  = regexp.MustCompile(`^[A-Fa-f0-9]{40}:\d+$`)
	FilePattern = regexp.MustCompile(`^[A-Za-z0-9_./-]+\.gno$`)
)
View Source
var (
	ErrInvalidOID    = errors.New("invalid object id")
	ErrInvalidTID    = errors.New("invalid type id")
	ErrInvalidFile   = errors.New("invalid file")
	ErrInvalidHeight = errors.New("invalid height")
	ErrInvalidLine   = errors.New("invalid line")
	ErrInvalidOffset = errors.New("invalid offset")
	ErrInvalidLimit  = errors.New("invalid limit")
	ErrInvalidSearch = errors.New("invalid search")
)

Functions

func AttachDocs

func AttachDocs(nodes []StateNode, vals []NamedDoc, funs []NamedDoc, typs []NamedDoc)

AttachDocs projects doc-index entries onto top-level StateNodes by Name. Only top-level nodes carry Names matchable to the doc index.

func CanonicalViewMode

func CanonicalViewMode(s string) string

CanonicalViewMode normalizes the ?state&view=… query param. URL-driven so the nginx cache key stays URL-only (no Vary: Cookie split).

func EnrichLinks(nodes []StateNode, pkgPath, viewMode string)

EnrichLinks walks the StateNode tree and populates Href + OwnerHref from ObjectID/OwnerID. Without this the template's `{{ if $n.Href }}` guards drop every Inspect / Owner / navlink button on the page.

func NewPageView

func NewPageView(data StateData) *components.View

NewPageView wraps the full-page state template so callers in gnoweb's Get pipeline can compose it inside IndexLayout via the standard (status, *components.View) return shape.

func ParseTrustedProxies

func ParseTrustedProxies(cidrs []string) ([]*net.IPNet, error)

ParseTrustedProxies parses CIDR strings into networks for TrustedProxies. A bare IP (no mask) is accepted and treated as a /32 or /128. Empty entries are skipped, but any unparseable entry returns an error so a misconfigured proxy list fails loudly at startup rather than silently dropping entries (and quietly narrowing or widening trust).

func PkgKindLabel

func PkgKindLabel(pkgPath string) string

PkgKindLabel returns "Realm" for `/r/` paths, "Package" otherwise.

func RealmStateHref

func RealmStateHref(pkgPath string) template.URL

RealmStateHref returns the URL of a package's top-level state page (`/r/foo$state`).

func ShortenOID

func ShortenOID(id, ref string) string

ShortenOID returns id's trailing `:N` when its hashlet matches ref's, otherwise the full id.

func TruncOID

func TruncOID(id string, head, tail int) string

TruncOID truncates an ObjectID's hashlet while preserving the `:N` suffix.

func ValidateFile

func ValidateFile(s string) error

func ValidateHeight

func ValidateHeight(s string) (int64, error)

ValidateHeight returns 0 for empty input (meaning "latest"). Mirrors weburl/parseHeight's rejection of sign prefixes and >19-char inputs so both code paths agree on what's valid.

func ValidateHeightFromURL

func ValidateHeightFromURL(u *weburl.GnoURL) (int64, error)

ValidateHeightFromURL mirrors GnoURL.Height()'s dual WebQuery/Query lookup but rejects malformed values instead of silently returning 0.

func ValidateLimit

func ValidateLimit(s string) (int, error)

ValidateLimit bounds the attacker-controlled `limit` pagination param. Empty input → maxTopLevelDecls (default page size). Anything else must parse to a positive integer; values above the cap silently clamp so the per-page fragment fan-out budget always holds.

func ValidateLine

func ValidateLine(s string) (int, error)

func ValidateOID

func ValidateOID(s string) error

func ValidateOffset

func ValidateOffset(s string) (int, error)

ValidateOffset bounds the attacker-controlled `offset` pagination param. Empty input → 0 (first page). Anything else must parse to a non-negative integer; oversize values survive validation because the page handler clamps offset to total after the decode (out-of-range pages render empty).

func ValidateSearch

func ValidateSearch(s string) (string, error)

ValidateSearch bounds the attacker-controlled `search` query param. Empty / whitespace-only → "" (no filter). Length cap makes the O(N×M) Contains scan cheap; control bytes are rejected to keep the query log-safe.

func ValidateTID

func ValidateTID(s string) error

ValidateTID bounds the attacker-controlled &tid= param. A Gno TypeID is a human-readable string (e.g. "gno.land/r/demo/foo.Bar", "int"), not a hash — so cap length and reject control chars, nothing more.

Types

type ClientAdapter

type ClientAdapter interface {
	Realm(ctx context.Context, path, args string) ([]byte, error)
	ListPaths(ctx context.Context, prefix string, limit int) ([]string, error)
	Doc(ctx context.Context, path string, height int64) (*doc.JSONDocumentation, error)
	StatePkg(ctx context.Context, path string, height int64) ([]byte, error)
	StateObject(ctx context.Context, oid string, height int64) ([]byte, error)
	StateType(ctx context.Context, typeId string, height int64) ([]byte, error)
}

ClientAdapter is the subset of gnoweb.ClientAdapter the state handler consumes. Declared locally so feature/state does not import the gnoweb package (handler_http.go in gnoweb imports state — a back import would create a cycle). Method set is a subset of gnoweb.ClientAdapter so a *gnoweb.MockClient or *gnoweb.rpcClient satisfies this contract.

type DecodedObject

type DecodedObject struct {
	Nodes []StateNode
	Info  StateObjectInfoView
}

DecodedObject is what handlers receive when calling DecodeObjectFull — the children to render plus the metadata to surface in the sidebar.

func DecodeObjectFull

func DecodeObjectFull(rawObject, rawType []byte, cfg RenderConfig) (decoded *DecodedObject, err error)

DecodeObjectFull parses qobject_json (and optional qtype_json) into children to render plus the queried object's ObjectInfo in one pass. cfg bounds recursion depth on both the typed and untyped paths so a &tid= request honours the same per-fragment budget as the untyped one. recoverToErr keeps amino + walker panics on hostile chain bytes inside the function — the page/fragment handler surfaces a clean error instead of unwinding to net/http's top-level recover.

type Deps

type Deps struct {
	Client      ClientAdapter
	Highlighter components.SnippetHighlighter
	// FileFetcher reads one source file by (pkgPath, fileName). Optional:
	// when nil, frag=source returns a fragment-error pointing at the
	// permanent ?source link. Wrapped per-request because the local
	// ClientAdapter cannot carry FileMeta without an import cycle.
	FileFetcher components.FileFetcher
	Logger      *slog.Logger
	// RateLimit configures the per-IP token bucket. Zero value disables it;
	// the limiter check in Handle becomes a no-op.
	RateLimit RateLimitConfig
}

Deps is a struct of interfaces so each field is independently mockable.

type FragErrorData

type FragErrorData struct {
	Message   string
	RetryHint string
}

FragErrorData feeds fragError. Always returned with HTTP 200 so htmx swaps the body instead of silently dropping a 4xx/5xx.

type FragNodeData

type FragNodeData struct {
	Node     StateNode
	PkgPath  string
	ViewMode string
	Depth    int
	// OID is the request's `?oid=…` — preserved separately because the
	// fragment's promoted root (func/closure inline) has no ObjectID of
	// its own. Drives the closure-tag OOB-swap target id.
	OID string
}

FragNodeData renders one node's content as a chrome-less HTML fragment via the shared state/nodes renderer. PkgPath + ViewMode keep nested permalinks correct; Depth is the parent row's tree depth so children indent via --depth.

type FragSourceData

type FragSourceData struct {
	SourceHTML template.HTML
	PkgPath    string
	File       string
	Line       int
}

FragSourceData feeds fragSource. SourceHTML is TRUSTED chroma markup — the template does not escape it. PkgPath builds the "See in code" permalink to the canonical full ?source view.

type Handler

type Handler struct {
	// contains filtered or unexported fields
}

func New

func New(deps Deps) *Handler

New validates required deps and returns a Handler. Panics if Client or Highlighter is nil; Logger falls back to slog.Default().

func (*Handler) Handle

Handle is the main entry for ?state* URLs. A nil view return means "body already written". The per-IP token-bucket runs first: on reject, htmx clients see a fragment-error (HTTP 200 + visible body); non-htmx clients get the standard 429 + Retry-After.

type IPLimiter

type IPLimiter struct {
	// contains filtered or unexported fields
}

IPLimiter is a per-IP token-bucket map with LRU bounded eviction. Safe for concurrent use. Construct with NewIPLimiter.

func NewIPLimiter

func NewIPLimiter(cfg RateLimitConfig) *IPLimiter

NewIPLimiter constructs a limiter from cfg. Returns nil when cfg.PerMinute <= 0 (caller treats nil as "open mode" — the limiter check is skipped).

func (*IPLimiter) Allow

func (l *IPLimiter) Allow(ip string) bool

Allow returns true if a request from ip is permitted right now. Side effect: refills then debits one token; touches LRU order; may evict.

type KindCounts

type KindCounts struct {
	All   int
	State int // struct, map, slice, array, pointer, ref
	Code  int // func, closure
	Types int // type, interface
}

KindCounts counts top-level declarations per filter-tab bucket. Buckets are usage-driven, not literal Kind names.

func ComputeKindCounts

func ComputeKindCounts(nodes []StateNode) KindCounts

ComputeKindCounts counts top-level Nodes into filter-tab buckets.

type NamedDoc

type NamedDoc struct {
	Name string
	Doc  string
}

NamedDoc is the (Name, Doc) pair the handler extracts from the JSON doc index — kept lightweight so the handler doesn't need to import the gnovm/doc package transitively into other layers.

type Pagination

type Pagination struct {
	Total       int
	StartNumber int // 1-based inclusive; may collapse to 0 on empty page
	EndNumber   int
	HasPrev     bool
	HasNext     bool
	FirstHref   template.URL
	PrevHref    template.URL
	NextHref    template.URL
	LastHref    template.URL
}

Pagination is the view-model for the top-level decls listing footer. Hrefs stay in the canonical `$webargs` grammar so navigation routes through the state handler and survives nginx caching.

type RateLimitConfig

type RateLimitConfig struct {
	PerMinute int // <=0 disables the limiter
	Burst     int // default = PerMinute
	MaxIPs    int // default 10_000
	// TrustedProxies is the set of trusted reverse-proxy networks. X-Real-IP
	// is honored ONLY when the connecting RemoteAddr falls inside one of
	// these CIDRs; empty = trust nothing (the safe default).
	TrustedProxies []*net.IPNet
	NowFunc        func() time.Time // injectable clock; default time.Now
}

RateLimitConfig configures the per-IP token bucket.

Budget math under the lazy-preview model: each pretty-view page-load debits 1 token for the SSR request + ~1 token per ref scrolled into view (fragment GETs). A page with N above-fold refs consumes 1+N tokens. With PerMinute=Burst=100 (default), a viewport-heavy page can consume a third of the budget in one paint — acceptable for a transitional defense-in-depth bucket. Primary HTTP rate-limit belongs to nginx; this limiter is the fallback when gnoweb is deployed alone.

Zero value is the "disabled" mode: NewIPLimiter returns nil and the limiter check is a no-op, allowing Deps to default-zero without touching it.

type RenderConfig

type RenderConfig struct {
	// MaxChildrenPerNode caps visible children; surplus collapses to one
	// KindTruncated sentinel.
	MaxChildrenPerNode int
	// MaxDecodeDepth bounds recursion depth for this single decode.
	MaxDecodeDepth int
}

RenderConfig bounds one Amino decode. Fragments use a shallow depth (≤3); the full-page path keeps walker.go's 256 for legacy parity.

func DefaultFragmentRenderConfig

func DefaultFragmentRenderConfig() RenderConfig

DefaultFragmentRenderConfig is the slim per-fragment budget (depth ≤3).

func DefaultPageRenderConfig

func DefaultPageRenderConfig() RenderConfig

DefaultPageRenderConfig is the full-depth budget for the legacy full-page path — parity with walker.go's package-wide constants.

type SourceLocation

type SourceLocation struct {
	File      string
	StartLine int
	EndLine   int
}

SourceLocation pinpoints a span in a source file.

type StateCrumb

type StateCrumb struct {
	Label string
	Href  template.URL
}

StateCrumb is a breadcrumb segment; Href is empty for the active segment.

type StateData

type StateData struct {
	PkgPath    string
	Nodes      []StateNode
	CountLabel string
	Crumbs     []StateCrumb
	Sidebar    *StateSidebar

	// ListHref is the current URL without `#fragment` — clicked to exit
	// the CSS focus mode by clearing the hash without a reload.
	ListHref template.URL

	// ViewMode is "tree" or "" (pretty default), derived from ?view= query param.
	ViewMode string

	// KindCounts feeds the kind-filter tab counters.
	KindCounts KindCounts

	// DocIndexJSON is the pre-marshaled qdoc projection over top-level
	// decls, embedded inline so the client-side controller can project
	// doc-comments onto htmx-loaded fragments without an extra RPC.
	DocIndexJSON template.JS

	// Pagination is the prev/next view-model for the top-level decls
	// footer. nil when total ≤ limit at offset 0 (no footer needed).
	Pagination *Pagination

	// SidebarTruncated is true when the full TOC exceeds maxSidebarTOC and
	// only the first cap entries are surfaced. The template renders a
	// "+N more — paginate to see them" hint when set.
	SidebarTruncated bool

	// SidebarTotal carries the realm's full top-level decl count so the
	// truncation hint can show the dropped-entry tail count (Total - cap).
	SidebarTotal int

	// SearchQuery is the validated `?search=` value. Empty when no filter
	// is active. Drives the banner + form input value in the template.
	SearchQuery string
}

StateData is the render payload for templates/page.html (renderPage) — field names must match the template.

type StateMetaEntry

type StateMetaEntry struct {
	Label string
	Value string
	// Href turns the value into a link when non-empty.
	Href template.URL
	// Mono renders blockchain IDs (OIDs, hashes) in truncated monospace.
	Mono bool
	// Section opens a new visual group; subsequent entries inherit it
	// until the next non-empty Section.
	Section string
	// Inline puts label + value on one line (compact).
	Inline bool
	// Block puts the value on its own row beneath the label (full-width).
	Block bool
}

StateMetaEntry is a single key/value fact in the sidebar.

type StateNode

type StateNode struct {
	Name       string
	Type       string
	Kind       string
	Value      string
	Expandable bool
	Children   []StateNode
	ObjectID   string
	TypeID     string
	Length     *int
	// Preview is a one-line summary of Children, rendered in
	// collapsed/ref rows. Re-computed after lazy ref fetches.
	Preview string
	Source  *SourceLocation
	// SourceHTML carries chroma-highlighted code; template.HTML so
	// html/template trusts it as already-safe markup.
	SourceHTML template.HTML
	// Href / OwnerHref are typed template.URL so html/template trusts them.
	Href      template.URL
	OwnerHref template.URL
	// Anchor is the row id stamped by Build{Package,Object}Sidebar for #
	// fragment linking from the TOC.
	Anchor string
	// ObjectInfo metadata captured by the walker from qobject_json/qpkg_json.
	Hash           string
	OwnerID        string
	ModTime        string
	RefCount       string
	LastObjectSize string
	// Doc is the plain-text source comment attached post-walk from the
	// package's JSON doc index, matched by Name. Rendered text-escaped
	// by the template — no Markdown processing.
	Doc string
}

StateNode is the UI-friendly decoded representation of a gno value. Built by the walker from raw Amino JSON; enriched post-walk with Href, SourceHTML, Doc, and Anchor by the orchestrator and sidebar builders.

func DecodeObject

func DecodeObject(ctx context.Context, raw []byte, cfg RenderConfig) (root StateNode, err error)

DecodeObject decodes one qobject_json payload into a root StateNode whose Children are the decoded fields/elements, bounded by cfg. Refs surface as KindRef; ExportRefValue cycle markers as KindCycle. recoverToErr keeps amino's hard panics on hostile chain bytes inside the function — the caller gets an error, never a torn request. The nil logger arg routes the panic payload into the wrapped err, picked up by the handler's existing decode-error log line.

func DecodePackage

func DecodePackage(ctx context.Context, raw []byte, cfg RenderConfig, offset, limit int) ([]StateNode, int, error)

DecodePackage decodes a paginated window over a qpkg_json payload's top-level slots, bounded by cfg. Returns (page, total, err); the caller builds the prev/next view-model from total via buildPagination. Splits into parsePackage + decodePackageSlice so the page handler can reuse the parsed Names+Values for the full sidebar TOC without re-decoding.

func (StateNode) Shape

func (n StateNode) Shape() string

Shape is the single source of truth for a node's render-shape, read by both the pretty-card and tree templates. branch = children already loaded; ref = expandable stored object fetched lazily on open; leaf = everything else.

type StateObjectInfoView

type StateObjectInfoView struct {
	Hash, OwnerID, ModTime, RefCount, LastObjectSize string
	IsEscaped                                        bool
}

StateObjectInfoView mirrors a stored object's ObjectInfo, formatted for display in the sidebar. The orchestrator extracts this from the queried object's outermost Value (qobject_json response).

type StateSidebar

type StateSidebar struct {
	Heading string
	TOC     []StateTOCEntry
	Meta    []StateMetaEntry
}

StateSidebar is the aside content next to the state tree.

func BuildObjectSidebar

func BuildObjectSidebar(pkgPath, oid, typeID string, info StateObjectInfoView, nodes []StateNode) *StateSidebar

BuildObjectSidebar assembles the aside for a per-object state page, grouping meta into Identity, Lineage, and Storage sections.

func BuildPackageSidebar

func BuildPackageSidebar(pkgPath string, nodes []StateNode) *StateSidebar

BuildPackageSidebar assembles the aside for a top-level state page.

func BuildPackageSidebarFull

func BuildPackageSidebarFull(pkgPath string, allNames, anchors, allKinds, allTypes []string, currentOffset, limit int) (*StateSidebar, bool)

BuildPackageSidebarFull assembles the sidebar with a TOC covering every top-level decl (capped at maxSidebarTOC), regardless of which slice the current page renders. On-page entries point to in-page anchors; off-page ones point to the cross-page state URL stamped with the right offset. Returns (sidebar, truncated) — truncated is true when allNames exceeds the cap, so the template can render the "+N more" hint.

type StateTOCEntry

type StateTOCEntry struct {
	Label      string
	Anchor     string
	Kind       string
	Type       string
	PrettyHref template.URL
	TreeHref   template.URL
	// OnPage marks entries that resolve to an in-page row id; off-page
	// entries set data-off-page="true" on the rendered <li>.
	OnPage bool
}

StateTOCEntry is a side-rail nav entry. Anchor matches `id="<anchor>"` on the corresponding row. PrettyHref/TreeHref are pre-computed by the sidebar builder: in-page anchors for on-page entries, cross-page `$state&offset=N#anchor` URLs for off-page ones — the template stamps them verbatim so it never has to know which kind it is rendering.

Jump to

Keyboard shortcuts

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