pluginhost

package
v0.38.1 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package pluginhost is the reusable, plugin-agnostic host glue for GoFastr heavy-JS plugins that run inside an opaque-origin sandboxed iframe.

It distils the platform machinery out of the wysiwyg plugin (the first such plugin) so a second heavy-JS plugin can reuse it instead of reimplementing the iframe / broker / manifest / capability / framing-header plumbing. See ../docs/design/protocol-v1.md for the authoritative protocol contract this package implements.

What lives here (the platform's job):

  • Manifest / ClientModule: the declarative description of a plugin's client module (entry document, sandbox policy, capabilities, schema).
  • AssetServer: serves a plugin's embedded client assets with the correct Content-Types AND the framing/CORP/CSP header relaxation GoFastr's global security middleware otherwise blocks (the client-side isolation contract).
  • Allow: the capability gate reusing battery/auth's resource:verb scopes.
  • MountMarker: the generic mount-marker + hidden-field HTML the generic broker scans for.
  • BrokerScriptURL / RegisterBrokerRoute / UIHostOption: serving and injection of the generic host broker (host/pluginhost.js).

A plugin (wysiwyg, and future plugins from Worker G) composes these pieces in its Init and ships a thin JS adapter that registers its plugin-specific event handlers with the generic broker (see BrokerRegistration).

Index

Constants

View Source
const (
	IsolationSandboxOpaque = "sandbox-iframe-opaque"

	// DefaultSandbox is the v1 sandbox policy: scripts only. The same-origin
	// token is NEVER added — that would de-opaque the frame and collapse the
	// isolation guarantee.
	DefaultSandbox = "allow-scripts"
)

Isolation constants. v1 has a single fixpoint: the plugin client runs in an opaque-origin sandboxed iframe (sandbox="allow-scripts" WITHOUT allow-same-origin), served same-origin. See protocol-v1.md §1.

View Source
const (
	DefaultDocID     = "demo"
	DefaultMinHeight = "240px"
)

Default mount-marker attribute values; plugins override via MountConfig.

View Source
const BrokerRouteMethod = "GET"

BrokerRouteMethod is the router method the broker route is registered under.

View Source
const BrokerScriptURL = "/__gofastr/plugin/host/pluginhost.js"

BrokerScriptURL is the platform route serving the generic host broker (host/pluginhost.js). It is shared by every heavy-JS plugin — the same script is injected once per host page and dispatches to per-plugin adapters.

Variables

This section is empty.

Functions

func Allow

func Allow(ctx context.Context, granted []string, required string) bool

Allow reports whether a plugin action requiring the `required` capability is permitted. It is the intersection of two sides and DEFAULT-DENIES:

  1. Module grant — `granted` is the capability set THIS plugin mount was granted by the host. It is the ceiling: a plugin can never exceed its declared grants, even under a session cookie. A nil/empty `granted` denies everything. Matching uses auth.ScopeMatch (the same resource:verb wildcard grammar as token scopes — NOT a weaker parallel matcher), so a granted "*:*" is the explicit "grant everything" (dev/trusted) form.
  2. Caller authority — the request's own scopes must also permit it. A scoped API token restricts BELOW the plugin grant; a session/JWT caller is unscoped, so the plugin grant is the binding limit.

This inverts the old behavior, where an unscoped session caller passed every capability (auth.HasScope returns true with no token): the plugin grant set now confines the untrusted plugin regardless of how the user authenticated.

func Guard

func Guard(granted []string, required string, next http.Handler) http.Handler

Guard is the enforcement chokepoint a plugin's privileged RPC/route mounts so a forgotten check fails CLOSED. It runs next only when Allow permits `required` for the `granted` set; otherwise it writes 403 with the E_CAPABILITY_DENIED code and does not call next.

func MountMarker

func MountMarker(cfg MountConfig) render.HTML

MountMarker renders the generic mount marker div plus any hidden inputs. The generic host broker scans for `[data-fui-plugin]` and, for each marker, looks up the registered adapter by the plugin name to build the sandboxed iframe.

All interpolated values are HTML-escaped via render.Escape. The marker is intentionally a plain div (the broker creates the iframe inside it) so it drops cleanly into any form.

func RegisterBrokerRoute

func RegisterBrokerRoute(rt *router.Router)

RegisterBrokerRoute serves the generic host broker at BrokerScriptURL on the given router. It is IDEMPOTENT: multiple plugins may call it from their Init and only the first registration lands (the router would otherwise panic on a duplicate pattern). Every plugin that mounts a sandboxed client should call this in Init so the host page can load the broker regardless of plugin load order.

func UIHostOption

func UIHostOption() uihost.Option

UIHostOption returns the uihost.Option that injects the generic host broker into every UIHost-rendered page. Apps using a UIHost pass this to uihost.New. A plugin that ships its own adapter should compose this with its adapter script (adapter LAST, so the generic broker has defined its registry first):

uihost.WithExtraScripts(pluginhost.BrokerScriptURL, myPlugin.BrokerScriptURL)

func WriteCapabilityDenied

func WriteCapabilityDenied(w http.ResponseWriter, capability string)

WriteCapabilityDenied writes the canonical 403 capability-denial response (JSON body carrying the E_CAPABILITY_DENIED code and the offending capability) so every plugin route denies uniformly.

Types

type AssetServer

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

AssetServer serves a plugin's embedded client assets with the correct Content-Types and the platform framing/CORP/CSP policy on framed assets. It is the client-side isolation contract, factored out of the wysiwyg plugin so every heavy-JS plugin reuses it instead of hand-rolling the header relaxation.

func NewAssetServer

func NewAssetServer(fsys fs.FS, prefix string, specs []AssetSpec) *AssetServer

NewAssetServer builds an AssetServer that reads the named specs lazily from fsys (an embed.FS sub or any fs.FS) and serves them under prefix. Files missing from fsys at request time yield a 404; for go:embed'd bundles that never happens. Call AssetServer.AddBytes for host-page scripts that live in a different embed root, then AssetServer.Register.

func (*AssetServer) AddBytes

func (s *AssetServer) AddBytes(path, contentType string, framed bool, b []byte)

AddBytes registers an asset from pre-loaded bytes at an explicit full route path. Use it for host-page scripts (the broker adapter) that are not part of the framed FS. framed should be false for host scripts.

func (*AssetServer) Register

func (s *AssetServer) Register(rt *router.Router)

Register mounts every asset on the router. It is safe to register multiple AssetServers on the same router as long as their paths do not collide (the router panics on duplicate patterns otherwise).

type AssetSpec

type AssetSpec struct {
	// Name is the filename within the AssetServer's fs.FS (e.g. "editor.html").
	// The route is registered at prefix + "/" + Name.
	Name string

	// ContentType is the exact Content-Type header (e.g.
	// "text/html; charset=utf-8").
	ContentType string

	// Framed marks the assets that make up the sandboxed plugin frame (the
	// frame document and its sub-resources). Framed assets get the
	// framing/CORP/CSP relaxation GoFastr's global security middleware
	// otherwise blocks (DECISIONS.md "Phase 0 — DONE" gotcha #1); non-framed
	// host-page scripts (the broker / adapter) are served plain.
	Framed bool
}

AssetSpec describes one asset served from a filesystem by AssetServer.

type Attribute

type Attribute struct {
	Name  string
	Value string
}

Attribute is a single HTML attribute on the mount marker. Plugins use it to add their own data-* attributes (e.g. wysiwyg's data-fui-plugin-for listing the hidden field names to sync).

type BrokerRegistration

type BrokerRegistration struct {
	Manifest Manifest                                 `json:"manifest"`
	Config   any                                      `json:"config,omitempty"`
	OnEvent  func(method string, params any, api any) `json:"-"`
}

BrokerRegistration documents the JavaScript shape a plugin adapter passes to window.__gofastrPluginHost.register(name, registration). It is defined here for reference (Worker G, IDE hover, and the platform contract); the generic broker consumes it on the client side, not Go.

window.__gofastrPluginHost.register("wysiwyg", {
  manifest: {                              // → [Manifest], serialised to JS
    entry:        "/…/editor.html",
    isolation:    "sandbox-iframe-opaque",
    sandbox:      ["allow-scripts"],
    capabilities: ["document:read", …],
    minHeight:    "240px",
    schema:       "wysiwyg-v1",
    title:        "WYSIWYG editor"
  },
  config: { … },                           // plugin blob bridged in init.config
  onEvent: function (method, params, api) {
    // api = { request, sendEvent, iframe, marker, form }
    // handle plugin-specific events: docChanged, save, requestUpload, …
    // and mirror the generic hooks if the e2e depends on plugin-named ones.
  }
});

The generic broker itself handles the protocol-level events (ready, resize, focusChanged, themeApplied, metric, bootError), runs the envelope + source check + ready→init handshake, and stashes the generic hooks (iframe.__pluginReady / __pluginProbes / __pluginTheme / __pluginLastMetric). It calls registration.onEvent for EVERY inbound event after its own handling, so an adapter can both handle its own methods and mirror the generic hooks under plugin-specific names the tests read.

type ClientModule

type ClientModule struct {
	// Name is the plugin name, also the data-fui-plugin attribute value the
	// mount marker carries and the generic broker dispatches on.
	Name string

	// Manifest describes the client module.
	Manifest Manifest

	// Assets is the (sub)filesystem holding the framed client assets
	// (editor.html / editor.js / editor.css). May be nil if the plugin serves
	// its assets itself.
	Assets fs.FS
}

ClientModule bundles a plugin name with its Manifest and the embedded asset filesystem the AssetServer serves. It is the unit a plugin registers with the platform. Worker G builds one of these per plugin.

func NewClientModule

func NewClientModule(name string, m Manifest, assets fs.FS) (ClientModule, error)

NewClientModule is the validating constructor for a ClientModule: it runs Manifest.Validate so a mis-configured plugin fails loudly at registration instead of silently mounting a bad frame. Plugins should build their module through this rather than a struct literal.

type Field

type Field struct {
	Name  string
	Value string
}

Field is a hidden input emitted after the mount marker. The generic broker creates the iframe inside the marker; plugins use the hidden inputs so a normal form POST / data-fui-rpc submit round-trips the canonical doc and its markdown sibling (protocol-v1.md §9).

type Manifest

type Manifest struct {
	// Entry is the frame document URL the broker loads into the iframe, e.g.
	// "/__gofastr/plugin/wysiwyg/editor.html". Required.
	Entry string `json:"entry"`

	// ScriptHash is an optional bundle hash used for cache-busting / SRI. v1
	// does not enforce it; the broker appends its own cache-buster.
	ScriptHash string `json:"scriptHash,omitempty"`

	// Isolation is the isolation model identifier. The v1 fixpoint is
	// [IsolationSandboxOpaque]. Empty defaults to it; any other value is
	// rejected by [Manifest.Validate].
	Isolation string `json:"isolation"`

	// Sandbox is the iframe sandbox token list. MUST contain "allow-scripts" and
	// MUST NOT contain "allow-same-origin" (enforced by [Manifest.Validate]).
	// Defaults to ["allow-scripts"] when empty.
	Sandbox []string `json:"sandbox"`

	// Capabilities is the default resource:verb grant set advertised to the
	// client in init.capabilities when the mount marker does not override it.
	Capabilities []string `json:"capabilities,omitempty"`

	// MinHeight is the initial iframe height before the first resize event.
	// Defaults to "240px" when empty.
	MinHeight string `json:"minHeight,omitempty"`

	// Schema is the interchange schema version bridged in init.schemaVersion
	// (e.g. "wysiwyg-v1").
	Schema string `json:"schema"`

	// Title is the iframe title attribute (accessibility). Defaults to
	// "Plugin" when empty.
	Title string `json:"title,omitempty"`
}

Manifest is the declarative description of a plugin's client module. It is generalised from the wysiwyg plugin's Phase-0 manifest (protocol-v1.md §1/§5) and doubles as the JSON blob the generic host broker reads to build the sandboxed iframe for each mount marker.

Fields are kept minimal and forward-compatible: unknown params on the wire are ignored by both sides (protocol-v1.md §3 envelope is frozen; method param payloads may grow).

func (Manifest) SandboxString

func (m Manifest) SandboxString() string

SandboxString returns the iframe `sandbox` attribute value. It is AUTHORITATIVE, not advisory: it always includes "allow-scripts" and always strips "allow-same-origin" (and any other same-origin-collapsing token), regardless of what the manifest carries. A mis-configured or tampered manifest therefore cannot produce a de-opaqued frame — the isolation invariant does not depend on anyone having called Manifest.Validate.

func (Manifest) Validate

func (m Manifest) Validate() error

Validate enforces the v1 isolation invariants, failing loudly at registration on a mis-configured manifest. It is called by NewClientModule. Note the frame's actual sandbox attribute is derived by Manifest.SandboxString / the broker's sandboxFor, both of which are authoritative (they strip allow-same-origin regardless), so Validate is a fail-fast nicety, not the sole line of defense. It does not mutate the receiver.

type MountConfig

type MountConfig struct {
	// Plugin is the plugin name — the data-fui-plugin attribute the generic
	// broker dispatches on to find the registered adapter. Required.
	Plugin string

	// DocID is the persistence key (data-fui-plugin-docid). Defaults to
	// [DefaultDocID].
	DocID string

	// MinHeight is the initial iframe height (data-fui-plugin-minheight).
	// Defaults to [DefaultMinHeight].
	MinHeight string

	// Capabilities is an optional CSV grant override
	// (data-fui-plugin-capabilities). Empty ⇒ adapter manifest default.
	Capabilities string

	// Doc is an optional initial document JSON server-rendered into the marker
	// (data-fui-plugin-doc) for reload round-trip.
	Doc string

	// Attributes are extra attributes appended to the marker (plugin-specific).
	Attributes []Attribute

	// Fields are hidden inputs emitted after the marker.
	Fields []Field
}

MountConfig configures MountMarker. It is the generic, plugin-agnostic shape; a plugin's own Mount wraps it (see the wysiwyg plugin for the pattern).

Jump to

Keyboard shortcuts

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