uinodev1

package
v0.37.0 Latest Latest
Warning

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

Go to latest
Published: Jul 20, 2026 License: MIT Imports: 5 Imported by: 0

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

View Source
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

func IsValidHostRelative(s string) bool

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

type DataColumn struct {
	Key   string `json:"key"`
	Label string `json:"label"`
}

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

type DetailItem struct {
	Label string `json:"label"`
	Value string `json:"value"`
}

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 EmProps

type EmProps struct {
	Text string `json:"text"`
}

EmProps is emphasized text.

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

type KeyValueItem struct {
	Key   string `json:"key"`
	Value string `json:"value"`
}

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

func Validate(data []byte, lim Limits) (*Tree, error)

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.

Jump to

Keyboard shortcuts

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