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
- Variables
- func AttachDocs(nodes []StateNode, vals []NamedDoc, funs []NamedDoc, typs []NamedDoc)
- func CanonicalViewMode(s string) string
- func EnrichLinks(nodes []StateNode, pkgPath, viewMode string)
- func NewPageView(data StateData) *components.View
- func ParseTrustedProxies(cidrs []string) ([]*net.IPNet, error)
- func PkgKindLabel(pkgPath string) string
- func RealmStateHref(pkgPath string) template.URL
- func ShortenOID(id, ref string) string
- func TruncOID(id string, head, tail int) string
- func ValidateFile(s string) error
- func ValidateHeight(s string) (int64, error)
- func ValidateHeightFromURL(u *weburl.GnoURL) (int64, error)
- func ValidateLimit(s string) (int, error)
- func ValidateLine(s string) (int, error)
- func ValidateOID(s string) error
- func ValidateOffset(s string) (int, error)
- func ValidateSearch(s string) (string, error)
- func ValidateTID(s string) error
- type ClientAdapter
- type DecodedObject
- type Deps
- type FragErrorData
- type FragNodeData
- type FragSourceData
- type Handler
- type IPLimiter
- type KindCounts
- type NamedDoc
- type Pagination
- type RateLimitConfig
- type RenderConfig
- type SourceLocation
- type StateCrumb
- type StateData
- type StateMetaEntry
- type StateNode
- type StateObjectInfoView
- type StateSidebar
- func BuildObjectSidebar(pkgPath, oid, typeID string, info StateObjectInfoView, nodes []StateNode) *StateSidebar
- func BuildPackageSidebar(pkgPath string, nodes []StateNode) *StateSidebar
- func BuildPackageSidebarFull(pkgPath string, allNames, anchors, allKinds, allTypes []string, ...) (*StateSidebar, bool)
- type StateTOCEntry
Constants ¶
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).
const ( MaxStateIDLength = 256 MaxFragmentLine = 1_000_000 MaxFragmentFileSize = 256 * 1024 MaxSearchQueryLen = 128 )
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.
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`.
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.
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 ¶
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.
var ( OIDPattern = regexp.MustCompile(`^[A-Fa-f0-9]{40}:\d+$`) FilePattern = regexp.MustCompile(`^[A-Za-z0-9_./-]+\.gno$`) )
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 ¶
AttachDocs projects doc-index entries onto top-level StateNodes by Name. Only top-level nodes carry Names matchable to the doc index.
func CanonicalViewMode ¶
CanonicalViewMode normalizes the ?state&view=… query param. URL-driven so the nginx cache key stays URL-only (no Vary: Cookie split).
func EnrichLinks ¶
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 ¶
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 ¶
PkgKindLabel returns "Realm" for `/r/` paths, "Package" otherwise.
func RealmStateHref ¶
RealmStateHref returns the URL of a package's top-level state page (`/r/foo$state`).
func ShortenOID ¶
ShortenOID returns id's trailing `:N` when its hashlet matches ref's, otherwise the full id.
func ValidateFile ¶
func ValidateHeight ¶
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 ¶
ValidateHeightFromURL mirrors GnoURL.Height()'s dual WebQuery/Query lookup but rejects malformed values instead of silently returning 0.
func ValidateLimit ¶
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 ValidateOID ¶
func ValidateOffset ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
New validates required deps and returns a Handler. Panics if Client or Highlighter is nil; Logger falls back to slog.Default().
func (*Handler) Handle ¶
func (h *Handler) Handle(ctx context.Context, w http.ResponseWriter, r *http.Request, u *weburl.GnoURL) (int, *components.View)
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).
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 ¶
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 ¶
SourceLocation pinpoints a span in a source file.
type StateCrumb ¶
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 ¶
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.
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.