pluginhost

package
v0.82.0 Latest Latest
Warning

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

Go to latest
Published: Sep 4, 2026 License: MIT Imports: 16 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.
  • CheckHostRequirements: boot-time warning when a plugin's declared host-page permission needs (Manifest.HostRequirements) are denied by the app's Permissions-Policy. Logs, never fails.
  • 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.

View Source
const FrameClientRouteMethod = "GET"

FrameClientRouteMethod is the router method the frame client route is registered under.

View Source
const FrameClientScriptURL = "/__gofastr/plugin/frame/frameclient.js"

FrameClientScriptURL is the platform route serving the frame-side channel client (frame/frameclient.js), the mirror of the host broker. Plugin frame documents hot-link it to get the same envelope, source check, and request/response semantics as the host side without hand-rolling a postMessage RPC per plugin.

View Source
const HostRequirementPrefix = "permissions-policy:"

HostRequirementPrefix is the grammar of a Manifest.HostRequirements token: "permissions-policy:<feature>", e.g. "permissions-policy:camera".

The prefix names WHERE the requirement lives — the app's Permissions-Policy response header — so the vocabulary can grow to other host-page surfaces later without repurposing bare words (a bare "camera" token would read as a frame capability, the opposite of what it is).

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 CheckHostRequirements added in v0.74.0

func CheckHostRequirements(log *slog.Logger, permissionsPolicy string, modules ...ClientModule)

CheckHostRequirements is the boot-time check for Manifest.HostRequirements. Pass the Permissions-Policy the app configures (the PermissionsPolicy field of core/middleware.SecurityHeadersConfig); an empty value is treated as the framework default, which denies camera, microphone and geolocation. There is no central ClientModule registry to hang this on — an app that mounts plugins calls it once at startup with its modules:

pluginhost.CheckHostRequirements(slog.Default(), secCfg.PermissionsPolicy, scanner, charts)

It LOGS and never fails: a plugin declaring a requirement the host has not satisfied is a developer-facing warning, and a plugin must not be able to take an app down by declaring something. It never returns an error and never panics for any input; tokens that do not parse (a module built as a struct literal, bypassing NewClientModule) are skipped silently — Manifest.Validate is the loud gate for those.

The "not satisfied" rule is deliberately narrow: warn only when EVERY directive naming the feature carries the empty allowlist "()" — the one Permissions-Policy shape that unambiguously denies the feature to every context, including the host page itself, and the shape GoFastr's default header uses. Anything else stays silent: "(self)" and "*" grant the page, a directive that does not name the feature leaves it at its default allowlist, and an origin list "(https://a.example)" cannot be decided at boot at all (it depends on the app's own origin, which no request has supplied yet). A warning that fired on grants would train developers to ignore the check; missing an exotic denial costs one console error, which is the status quo this check improves on.

func FrameClientJS added in v0.74.0

func FrameClientJS() []byte

FrameClientJS returns a copy of the embedded frame client, for plugins that bundle the script into their own frame document instead of hot-linking FrameClientScriptURL.

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 RegisterFrameClientRoute added in v0.74.0

func RegisterFrameClientRoute(rt *router.Router)

RegisterFrameClientRoute serves the frame client at FrameClientScriptURL on the given router. Like RegisterBrokerRoute it is IDEMPOTENT: multiple plugins may call it from their Init and only the first registration lands.

Unlike the broker (a HOST-page script served same-origin, framed=false), this script is fetched BY opaque-origin frame documents: the global security middleware's Cross-Origin-Resource-Policy: same-origin would refuse the "null"-origin frame's <script src>, so it is served with framed=true to apply exactly that CORP relaxation. The framedCSP header that writeAsset also emits is inert on a script response (CSP governs documents, not script bytes) — harmless there; the CORP relaxation is the point. The platform's own route serves no per-plugin manifest, so it passes no CSP extensions: the default framed policy.

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 AssetOption added in v0.80.0

type AssetOption func(*assetOptions)

AssetOption customises one byte-backed asset registered through AssetServer.AddBytes. Options are validated there, at registration: an invalid combination panics at boot with the cause, the same posture AssetServer.Register takes on a nil fs.FS.

func WithCache added in v0.80.0

func WithCache(c CacheProfile) AssetOption

WithCache sets an explicit cache posture for the asset. Works on any byte-backed asset, worker or host script; without it the asset keeps CacheDefault.

func WithWorkerCSP added in v0.80.0

func WithWorkerCSP(p WorkerCSP) AssetOption

WithWorkerCSP marks the asset as a trusted host-page worker and sets its response CSP profile (see WorkerCSP). Its presence IS the worker kind: framed assets keep the fixed platform policy (passing it together with framed=true panics at registration), and a byte asset without it stays a plain host script with no policy of its own.

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(route, contentType string, framed bool, b []byte, opts ...AssetOption)

AddBytes registers an asset from pre-loaded bytes at an explicit full route path. Use it for host-page scripts (the broker adapter) and trusted host-page workers that are not part of the framed FS. framed should be false for host scripts. An empty contentType is derived from route's extension, as for AssetSpec.ContentType.

opts customise the asset: WithWorkerCSP marks a trusted host-page worker and names the narrow policy its own response carries, WithCache sets an explicit cache posture. Both are validated HERE, at registration — a token outside the allowlists, a worker profile on a framed asset, or an unknown cache profile panics at boot with the cause, the same posture AssetServer.Register takes on a nil fs.FS.

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).

Specs with no filesystem to read them from are rejected here, at boot. Without this they registered fine and took down whichever request first asked for one, because fs.ReadFile on a nil fs.FS dereferences the nil interface. Boot is also the right place rather than the quieter repair of serving 404 per request. ClientModule.Assets is documented as optional — a plugin may serve its own assets — but then it does not pass specs to an AssetServer either, so a nil FS carrying specs is always a wiring mistake and never a runtime condition: the specs are right there in the same call, and a 404 on the frame document would make it one more construction that validates, registers, serves, and yields a frame that cannot work — the failure class AssetSpec.ContentType and Manifest.CSP already cost a debugging cycle each. A nil FS with no specs is the legitimate byte-backed server (AssetServer.AddBytes only) and is left alone.

func (*AssetServer) WithCSP added in v0.74.0

func (s *AssetServer) WithCSP(tokens []string) *AssetServer

WithCSP sets the per-plugin CSP keyword extensions (from Manifest.CSP) appended to framed assets' script-src, returning s for chaining:

srv := pluginhost.NewAssetServer(fsys, prefix, specs).WithCSP(mod.Manifest.CSP)

Tokens are re-filtered through [allowedCSPKeywords] when the header is assembled, so a slice that skipped Manifest.Validate cannot smuggle a keyword. nil (the default, i.e. no call) produces the same header as a plugin without the tier.

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"). Optional: when empty it is derived from
	// Name's extension by [static.DetectFromName]. Set it only to override
	// that default, e.g. to serve a ".js" file as "application/json".
	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.
  },
  onRequest: function (method, params, api) {
    // static fallback for frame → host requests with no explicit
    // api.onRequest handler; return a value or a Promise (a throw
    // becomes an E_HANDLER response). Neither exists → E_NO_HANDLER.
  }
});

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. Inbound frame → host requests dispatch to a per-method api.onRequest handler first, with registration.onRequest as the fallback, and are ALWAYS answered.

type CacheProfile added in v0.80.0

type CacheProfile int

CacheProfile names the Cache-Control posture for one byte-backed asset. An enum, not a string: the exact header text stays the server's decision, so a profile cannot smuggle a directive (a hand-written "public" on an authenticated page's worker, say). CacheDefault keeps the posture every asset had before profiles existed.

const (
	// CacheDefault is the pre-profile posture: "no-store, max-age=0". These
	// dev assets carry no cache validator and are referenced by
	// un-versioned paths, so a stale browser copy must not linger across
	// rebuilds. Byte-identical to what every asset served before profiles
	// existed; the default cannot drift.
	CacheDefault CacheProfile = iota

	// CachePublicImmutable is "public, max-age=31536000, immutable": shared
	// caches along the path may store the bytes and never revalidate within
	// a year. Only for URLs whose path changes when the bytes change (a
	// content hash) AND whose bytes are secret-free — public means any
	// intermediary may keep a copy.
	CachePublicImmutable

	// CachePrivateRevalidate is "private, no-cache": the browser may store
	// the asset but MUST revalidate before every reuse. The auth-compatible
	// middle ground — no shared cache ever sees it, and a logged-out or
	// revoked session stops getting fresh copies.
	CachePrivateRevalidate

	// CachePrivateNoStore is "private, no-store": nothing stores it,
	// anywhere, ever. For worker bytes that ship per-session or that must
	// not survive a logout on a shared machine.
	CachePrivateNoStore
)

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.

func (ClientModule) AssetServer added in v0.75.0

func (m ClientModule) AssetServer(prefix string, specs []AssetSpec) *AssetServer

AssetServer builds the AssetServer for this module's framed assets: it reads from ClientModule.Assets and threads Manifest.CSP through AssetServer.WithCSP, so a manifest that declares the wasm tier and the server that answers for the frame cannot disagree.

Prefer it over calling NewAssetServer directly. CSP is the one manifest field applied as a response header rather than carried on the manifest object to the mount, so a host that builds its asset server separately can declare the tier, pass Validate, and still serve a frame that refuses to compile WebAssembly, with the failure surfacing as a CompileError inside an opaque frame that has no way to report it:

srv := mod.AssetServer("/__gofastr/plugin/sql", specs)

NewAssetServer remains for hosts serving assets that belong to no module.

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"`

	// CSP lists opt-in Content-Security-Policy keywords appended to the framed
	// policy's script-src. It reaches the frame when the asset server is built
	// from the module — [ClientModule.AssetServer] threads it — because CSP is
	// the one manifest field applied as a response header rather than carried
	// on the manifest object to the mount. A host that instead calls
	// [NewAssetServer] directly must pass it through [AssetServer.WithCSP], or
	// the manifest validates, the frame still refuses WebAssembly, and nothing
	// reports why. The allowlist is closed and has exactly one member:
	// 'wasm-unsafe-eval', which lets a plugin compile WebAssembly inside the
	// sandboxed frame without granting
	// string eval ('unsafe-eval' stays forbidden) and without touching any
	// other directive — the frame keeps its opaque origin, sandbox
	// allow-scripts, and connect-src 'none', so a wasm engine still exchanges
	// data only over the postMessage bridge. Anything outside the allowlist
	// (a host source, 'unsafe-inline', 'unsafe-eval', '*', or a token
	// carrying ';', whitespace, or mismatched quotes — these values are
	// interpolated into a response header, where ';' could splice an
	// arbitrary directive such as re-enabled connect-src) is rejected by
	// [Manifest.Validate] and dropped at header assembly. Matching is EXACT,
	// byte-for-byte: unlike the HTML sandbox attribute the CSP header neither
	// case-folds nor whitespace-tokenises source expressions, so a variant
	// like 'WASM-UNSAFE-EVAL' grants nothing — and exact match rejects every
	// smuggle shape with the one comparison.
	CSP []string `json:"csp,omitempty"`

	// 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"`

	// HostRequirements names browser features the HOST PAGE around the plugin
	// must be allowed to use, as "permissions-policy:<feature>" tokens (e.g.
	// "permissions-policy:camera" for a scanner whose host page captures and
	// whose sandboxed frame decodes). The frame itself is opaque-origin and
	// can never hold these permissions; this declares what the page embedding
	// it needs, so [CheckHostRequirements] can turn an unsatisfied token into
	// a boot-time warning instead of the runtime console error a user would
	// otherwise hit first. Validated against a closed feature registry.
	HostRequirements []string `json:"hostRequirements,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

	// Fallback is server-rendered HTML placed inside the marker as the
	// pre-hydration state (wrapped in a div carrying
	// data-fui-plugin-fallback). The broker hides the iframe until the
	// frame reports ready, then hides the fallback — hidden, never
	// removed — and shows the frame; on bootError it swaps back, so a
	// plugin with a Go-side renderer (the chart plugin's SSR SVG)
	// degrades to its static output instead of an empty box, and works
	// with JavaScript off. Teardown restores it until SPA nav replaces
	// the marker.
	//
	// TRUST: this is render.HTML in the host page's own trust domain,
	// emitted verbatim like every render.HTML — the plugin's Go-side
	// Mount() builds it server-side; it never comes from the frame.
	Fallback render.HTML

	// 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).

type WorkerCSP added in v0.80.0

type WorkerCSP struct {
	// ScriptKeywords appends keywords to the worker policy's script-src.
	// Allowlist: 'unsafe-eval' (runtime compilation — ONNX Runtime glue,
	// asm.js builds, and the wasm gate on browsers that predate
	// 'wasm-unsafe-eval') and 'wasm-unsafe-eval' (WebAssembly compilation
	// only, the narrower grant when no string eval is needed). The base
	// 'self' is always present and cannot be removed.
	ScriptKeywords []string

	// ConnectSources widens the worker policy's connect-src from its
	// 'none' default. Allowlist: 'self', for fetching the wasm binary and
	// model bytes from the app's own origin. A CDN, wildcard, or remote
	// endpoint is refused: applications pin their runtimes same-origin
	// (the plugin-platform doc carries the delivery recipe), and this
	// server is not a remote-artifact proxy.
	ConnectSources []string

	// WASM appends 'wasm-unsafe-eval' — WebAssembly compilation without
	// string eval. Prefer it over ScriptKeywords when the worker only
	// compiles; add 'unsafe-eval' only when the runtime actually needs it.
	WASM bool
}

WorkerCSP is the validated Content-Security-Policy profile for a trusted host-page worker's OWN script response. It is the third asset shape an AssetServer serves, beside host scripts (no policy of their own) and framed plugin assets ([framedCSP]): a worker the app compiles in and vouches for — an OpenCV or ONNX depth worker, say — running heavyweight code that needs runtime compilation the host document must never grant.

The profile widens ONLY the worker's own response. A dedicated worker enforces the CSP delivered with its script, not the document's, so 'unsafe-eval' can live on the worker response alone while the host page keeps the app's strict policy byte-for-byte. Register one with AssetServer.AddBytes and framed=false:

srv.AddBytes("/__w/depth.js", "text/javascript; charset=utf-8", false, workerJS,
	WithWorkerCSP(WorkerCSP{
		ScriptKeywords: []string{"'unsafe-eval'"},
		ConnectSources: []string{"'self'"},
		WASM:           true,
	}),
	WithCache(CachePrivateNoStore))

Every field is matched byte-for-byte against a closed allowlist at registration; a token carrying ';', whitespace, a host source, or a wildcard is rejected there and dropped again at assembly (see [workerCSP]) — the same double gate Manifest.CSP and framedCSP use.

Jump to

Keyboard shortcuts

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