Documentation
¶
Overview ¶
Package uinodev1 implements the closed ui.node.v1 wire type and validator for process-isolated third-party modules (design §9, issue #37).
A module returns a JSON tree conforming to ui.node.v1 over the module protocol's ui.node.v1 body kind. The host validates it with Validate BEFORE mapping it to design-system components and rendering.
Why a distinct wire type (not core-ui/node.Node) ¶
core-ui/noderender is a *denylist* built for first-party IR: arbitrary props reach HTML attributes through its extraAttrs passthrough, which drops only style, srcdoc, and on* and forwards everything else verbatim. That lets a third party forge the exact trusted-runtime attributes (data-fui-rpc, data-fui-*) that runtime.js acts on — a trusted-channel forgery that CSP does not catch (design §9).
This package's wire type makes the attack UNREPRESENTABLE rather than denied: there is no map[string]any prop bag, no Bindings, no Actions, and no passthrough. Props are a closed union of per-component structs, each carrying only typed scalar fields the host mapping needs. Every unknown field rejects the whole tree (via json.DisallowUnknownFields).
Responsibility split ¶
This package owns:
- JSON decode (single bounded walk, with depth/node/child/text caps),
- Component enum check (closed allowlist; unknown ⇒ whole-tree reject),
- Typed prop validation (per-component struct, DisallowUnknownFields),
- URL guard for `to` props (host-relative same-origin paths only),
- Action reference shape check (non-empty, ≤128 chars, no whitespace),
- Whole-tree fail-closed caps (see DefaultLimits / Limits).
The HOST RENDERER owns (deliberately out of scope for this package):
- Mapping components to framework/ui + core-ui/html primitives,
- Assigning every id / class / ARIA attribute itself (modules cannot),
- Resolving ActionRef values to real data-fui-rpc URLs against the module descriptor's installed routes (see Renderer).
ActionRef values are OPAQUE STRINGS here. The validator checks only their shape. Resolution against installed routes is a host-side concern that lands in a later wave alongside the Renderer implementation, because route resolution requires the descriptor + router plumbing that does not exist yet in this package's dependency graph.
Input framing ¶
Validate accepts a []byte whose length is bounded by Limits.MaxInputBytes (default 1 MiB, mirroring core/mcp's maxMCPBodyBytes). Callers receiving the tree from an unbounded source (e.g. an io.Reader from the wire) MUST cap the reader first — e.g. via io.LimitReader — so a hostile publisher cannot exhaust host memory before Validate even runs.
Index ¶
- Constants
- func IsValidHostRelative(s string) bool
- type BadgeProps
- type ButtonProps
- type CardProps
- type ClusterProps
- type CodeProps
- type Component
- type DataCell
- type DataColumn
- type DataRow
- type DataTableProps
- type DetailItem
- type DetailListProps
- type DividerProps
- type EmProps
- type GridProps
- type HeadingProps
- type ImageProps
- type KeyValueItem
- type KeyValueProps
- type Limits
- type LinkProps
- type Node
- type ParagraphProps
- type Props
- type Renderer
- type SectionProps
- type SmallProps
- type StackProps
- type StatCardProps
- type StrongProps
- type TextProps
- type Tree
Constants ¶
const ( DefaultMaxDepth = 32 DefaultMaxNodes = 500 DefaultMaxChildrenPerNode = 128 DefaultMaxPropString = 4 * 1024 // 4 KiB DefaultMaxTotalText = 256 * 1024 // 256 KiB DefaultMaxInputBytes = 1 << 20 // 1 MiB DefaultMaxActionRefLen = 128 )
Default caps (design §9). These are intentionally tight: a module screen is bounded; if a real screen needs more, the host can pass a higher Limits value, but the defaults fail closed.
Variables ¶
This section is empty.
Functions ¶
func IsValidHostRelative ¶
IsValidHostRelative reports whether s is a host-relative same-origin path that is safe to use as a link/image target produced by a third-party module. This is the semantic URL guard required by design §9 — it is NOT a substitute for CSP, and CSP is not a substitute for it.
Acceptance criteria (all must hold):
- s is non-empty.
- s starts with a single "/": absolute same-origin path syntax.
- s does NOT start with "//": that is scheme-relative (e.g. "//evil.com/x") and would be resolved against an attacker's origin.
- s contains no scheme: any "scheme:" form (javascript:, data:, vbscript:, blob:, file:, http:, https:, …) requires the bytes before the first ":" to be a valid scheme, which is impossible here because s starts with "/". We additionally reject any dangerous scheme token appearing later in the string as defense-in-depth (see hasDangerousScheme).
- s contains no backslashes ("\"): browsers treat "\" as "/" in some URL-parsing contexts (the "magic backslash" bug), which can defeat scheme checks.
- s contains no whitespace, control bytes, or DEL: these are used to smuggle past parsers and to construct header-splitting / redirect-to-evil payloads.
Rejected examples (non-exhaustive):
"" // empty "foo" // relative, not host-relative "//evil.com/x" // scheme-relative "/\\evil.com" // backslash smuggling "/path\nwith-newline" // control char "javascript:alert(1)" // scheme (also fails the leading-slash check) "https://example.com" // absolute off-origin (fails leading-slash) "data:text/html,..." // scheme "vbscript:msgbox" // scheme "blob:..." // scheme "file:///etc/passwd" // scheme
Accepted examples (non-exhaustive):
"/" "/dashboard" "/users/42/edit" "/search?q=hello&pg=2" "/path/with-dots/../up"
Types ¶
type BadgeProps ¶
type BadgeProps struct {
Text string `json:"text"`
Tone string `json:"tone,omitempty"` // neutral | positive | negative | warning | info
}
BadgeProps is a small status label.
type ButtonProps ¶
type ButtonProps struct {
Label string `json:"label"`
Variant string `json:"variant,omitempty"` // primary | secondary | ghost | danger
}
ButtonProps configures a button. The action is referenced via the Node's ActionRef field (not in props) — a button without an ActionRef is rejected by the validator.
type CardProps ¶
type CardProps struct {
Title string `json:"title,omitempty"`
Elevation string `json:"elevation,omitempty"` // "flat" | "low" | "high"
}
CardProps configures a card surface.
type ClusterProps ¶
type ClusterProps struct {
Gap string `json:"gap,omitempty"`
Align string `json:"align,omitempty"`
}
ClusterProps configures a wrapping cluster (flex wrap).
type CodeProps ¶
type CodeProps struct {
Text string `json:"text"`
}
CodeProps is inline or block code.
type Component ¶
type Component string
Component is the closed allowlist of semantic components a ui.node.v1 tree may reference. Anything not in this enum is whole-tree rejected at decode time (design §9). The string values are the canonical wire names (lowercase, hyphenated); case-variants and typos do NOT match.
const ( // Layout components — accept children. CompStack Component = "stack" CompCluster Component = "cluster" CompGrid Component = "grid" CompSection Component = "section" CompCard Component = "card" CompDivider Component = "divider" // Text components — no children (text comes via props). CompHeading Component = "heading" CompParagraph Component = "paragraph" CompText Component = "text" CompStrong Component = "strong" CompEm Component = "em" CompCode Component = "code" CompSmall Component = "small" CompBadge Component = "badge" // Read-only data components — no children. CompDetailList Component = "detail-list" CompKeyValue Component = "key-value" CompStatCard Component = "stat-card" CompDataTable Component = "data-table" // Interactive components — by reference only. // A button carries an ActionRef at the Node level. // A link carries either To (host-relative) in Props or an ActionRef. CompButton Component = "button" CompLink Component = "link" // Media component — no children. // Src is a host-relative same-origin path (URL-guarded); Alt is // required non-empty so every module image is accessible. CompImage Component = "image" )
type DataCell ¶
type DataCell struct {
Text string `json:"text"`
}
DataCell is one scalar cell in a data-table row.
type DataColumn ¶
DataColumn is one column definition for a data-table.
type DataRow ¶
type DataRow struct {
Cells []DataCell `json:"cells"`
}
DataRow is one row of cells in a data-table. The validator does NOT enforce cell-count == column-count here; the host renderer is expected to handle short/long rows defensively. (v1 minimal: shape only.)
type DataTableProps ¶
type DataTableProps struct {
Columns []DataColumn `json:"columns"`
Rows []DataRow `json:"rows"`
}
DataTableProps configures a read-only table.
type DetailItem ¶
DetailItem is one row of a detail-list.
type DetailListProps ¶
type DetailListProps struct {
Items []DetailItem `json:"items"`
}
DetailListProps is a label/value list.
type DividerProps ¶
type DividerProps struct{}
DividerProps has no fields — a divider is purely structural.
type GridProps ¶
type GridProps struct {
Columns int `json:"columns,omitempty"` // 1..12; 0 = host default
Gap string `json:"gap,omitempty"`
}
GridProps configures a column grid.
type HeadingProps ¶
type HeadingProps struct {
Level int `json:"level"` // 1..6, required
Text string `json:"text"` // required
}
HeadingProps configures a heading. Level (1–6) and Text are required.
type ImageProps ¶
type ImageProps struct {
Src string `json:"src"` // required, host-relative same-origin path
Alt string `json:"alt"` // required, non-empty accessible description
}
ImageProps configures an image. Src is a host-relative same-origin path (validated by IsValidHostRelative — design §9 rejects javascript:/data:/ vbscript:/blob:/file:/off-origin); Alt is required non-empty so every module-supplied image carries accessible alternative text. A module cannot forge dimensions, loading, or srcset — those are host-assigned.
type KeyValueItem ¶
KeyValueItem is one entry of a key-value list.
type KeyValueProps ¶
type KeyValueProps struct {
Items []KeyValueItem `json:"items"`
}
KeyValueProps is a flat key/value map rendered as a list.
type Limits ¶
type Limits struct {
// MaxDepth bounds the tree's nesting depth (root = depth 1).
// Default 32.
MaxDepth int
// MaxNodes bounds the total number of nodes in the tree.
// Default 500.
MaxNodes int
// MaxChildrenPerNode bounds the number of children a single node
// may have. Default 128.
MaxChildrenPerNode int
// MaxPropString bounds the length (bytes) of any single string
// field in any prop struct. Default 4096 (4 KiB).
MaxPropString int
// MaxTotalText bounds the sum of every string field across the
// whole tree (a conservative upper bound; the actual rendered text
// is smaller because not every field becomes visible text).
// Default 262144 (256 KiB).
MaxTotalText int
// MaxInputBytes bounds the raw JSON input length. Default 1048576
// (1 MiB). Callers receiving the tree from an unbounded source MUST
// ALSO cap the source (e.g. io.LimitReader) so a hostile publisher
// cannot exhaust memory before Validate runs.
MaxInputBytes int
// MaxActionRefLen bounds the length of an ActionRef string.
// Default 128.
MaxActionRefLen int
}
Limits are the whole-tree fail-closed caps (design §9). All caps are enforced BEFORE the validator walks the tree (for input bytes) and DURING the walk (for depth / nodes / children / per-prop strings / total text). On any overflow, Validate rejects the whole tree — it never truncates.
The zero-value Limits uses DefaultLimits for every field that is unset (zero), so callers may set only the caps they want to override. Pass DefaultLimits explicitly to get the documented defaults.
func DefaultLimits ¶
func DefaultLimits() Limits
DefaultLimits returns the documented default caps. Validate applies these automatically when given a zero-value Limits; callers only need to call this explicitly when they want to inspect or fork the defaults.
type LinkProps ¶
type LinkProps struct {
Text string `json:"text"`
To string `json:"to,omitempty"` // host-relative path; empty if ActionRef is used
}
LinkProps configures a link. Either To (host-relative) is set in props, OR the Node carries an ActionRef; exactly one is required.
type Node ¶
type Node struct {
// Component is the closed enum value (one of the Comp* constants).
Component Component
// Props is the typed, per-component prop struct. Never nil for a
// decoded node — a component with no fields uses its zero-value
// struct (e.g. DividerProps{}).
Props Props
// Children is the optional list of child nodes. Only layout
// components accept children; the validator rejects children on
// text / data / interactive components.
Children []Node
// ActionRef is an opaque reference to an action/route the host
// renderer resolves against the module's installed routes. Required
// on button; optional-but-mutually-exclusive with Props.To on link.
// Empty on every other component. The validator checks only its
// shape here; resolution is the host renderer's job.
ActionRef string
}
Node is a single element in a validated ui.node.v1 tree.
Props is a sealed union — only the concrete prop types in this file implement it. There is no escape hatch: a Bindings/Actions/free-bag node is unrepresentable, and any unknown JSON field rejects the whole tree (see Validate).
type ParagraphProps ¶
type ParagraphProps struct {
Text string `json:"text"`
}
ParagraphProps is a paragraph body.
type Props ¶
type Props interface {
// contains filtered or unexported methods
}
Props is the sealed union of per-component prop structs. Every concrete implementation lives in this file; external packages cannot add new prop types. Each implementation's validate method enforces per-component invariants (URL scheme, range bounds, enum values, child policy).
type Renderer ¶
type Renderer interface {
// Render converts a validated tree to HTML. The returned HTML is
// trusted by the host's renderer pipeline (it produced it); callers
// must not feed unvalidated input through this interface — always
// run [Validate] first and pass the resulting [Tree].
Render(t *Tree) (render.HTML, error)
}
Renderer maps a validated ui.node.v1 tree to host-owned HTML.
The contract a Renderer implementation MUST honor:
It maps each Component to a framework/ui or core-ui/html primitive, assigning every id, class, ARIA attribute, and visual variant itself. Modules cannot influence these — there are no such fields on any prop struct by design.
It resolves every Node.ActionRef to a real data-fui-rpc URL by looking the ref up in the module descriptor's installed route table. An ActionRef that does not resolve to an installed route MUST be rejected, never improvised.
It MUST reject (return a non-nil error) rather than improvise for any node whose Component or props it does not know how to map. The validator's closed enum guarantees the component set is bounded, but the renderer still owns the mapping table and may further restrict it.
The Renderer interface is defined here so the validator and the renderer share a single typed Tree. No implementation ships in this package yet; it lands with the framework/ui serialization wave (design §9).
type SectionProps ¶
type SectionProps struct {
Title string `json:"title,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
}
SectionProps configures a titled section landmark.
type SmallProps ¶
type SmallProps struct {
Text string `json:"text"`
}
SmallProps is fine-print text.
type StackProps ¶
type StackProps struct {
Direction string `json:"direction,omitempty"` // "horizontal" | "vertical" (default vertical)
Gap string `json:"gap,omitempty"` // free-form token-ish string, bounded
Align string `json:"align,omitempty"` // "start" | "center" | "end" | "stretch"
}
StackProps configures a vertical-or-horizontal stack (flex column/row).
type StatCardProps ¶
type StatCardProps struct {
Label string `json:"label"`
Value string `json:"value"`
Unit string `json:"unit,omitempty"`
Trend string `json:"trend,omitempty"` // up | down | flat
}
StatCardProps is a single labeled metric.
type StrongProps ¶
type StrongProps struct {
Text string `json:"text"`
}
StrongProps is strongly-emphasized text.
type TextProps ¶
type TextProps struct {
Text string `json:"text"`
}
TextProps is bare inline/block text.
type Tree ¶
type Tree struct {
Root Node
}
Tree is the validated root of a ui.node.v1 document. Returned by Validate only when every node, prop, URL, and cap check has passed.
func Validate ¶
Validate decodes, type-checks, and bounds-checks a ui.node.v1 JSON tree. It returns the typed Tree on success, or a descriptive error describing the first failure encountered. On any failure the whole tree is rejected — Validate never returns a partial tree and never truncates content.
All whole-tree caps (Limits) are enforced: input bytes are bounded before any decode allocation; depth, node count, and per-node children are counted during the single recursive walk; per-prop strings and total text are accumulated during the same walk; URL props and ActionRef values are checked per-component. See Limits for the default cap values and how to override them.
data MUST be a single JSON object (the tree root). null, arrays, scalars, empty input, or input with trailing garbage after the root object are all rejected.