state

package
v0.0.0 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 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"
	ClientErrFileNotFound     = "file 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), and so feature/omnisearch classifies the same errors the same way.

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 FromTrustedProxy

func FromTrustedProxy(r *http.Request, trusted []*net.IPNet) bool

FromTrustedProxy reports whether r arrived from inside one of the trusted networks, so its forwarding headers (X-Real-IP, X-Forwarded-*) can be believed. An empty list trusts nothing.

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.

func (*IPLimiter) AllowRequest

func (l *IPLimiter) AllowRequest(r *http.Request) bool

AllowRequest resolves the client address and charges its bucket.

Which address to trust depends on which proxies to trust, and the limiter is the thing that was configured with them — so the rule lives here rather than being copied into every feature that rate-limits.

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. gnoweb defaults to PerMinute=Burst=1200, the edge's 20 req/s, so a viewport-heavy page costs a few percent of the budget. Primary HTTP rate-limit belongs to the reverse proxy, which sees the real client; 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