Documentation
¶
Overview ¶
Package webui serves the built to-do application out of the binary.
It is an inbound adapter like rest, mcp and calendar (ADR-0001), and it is the thinnest one: it answers with bytes and reaches nothing. No use case is registered for it, because serving a file is not a business operation - `usecase.Registry` stays the catalogue of things a person or an agent can ask the product to do.
Why the bundle is in the binary at all is ADR-0028. In short: the README promises self-hosting in two containers, and a UI container plus a proxy would be four - and would make it possible to run a UI against an API of a different version. Both come from the same commit here, so that cannot happen.
Index ¶
Constants ¶
const ContentSecurityPolicy = "default-src 'none'; " +
"script-src 'self'; " +
"style-src 'self'; " +
"img-src 'self' data: blob:; " +
"font-src 'self'; " +
"connect-src 'self'; " +
"manifest-src 'self'; " +
"worker-src 'self' blob:; " +
"base-uri 'none'; " +
"form-action 'none'; " +
"frame-ancestors 'none'"
ContentSecurityPolicy is the policy for the UI origin, decided in ADR-0028 - as it stands for an installation whose media is this server's own (`local`). PolicyFor widens exactly two of its directives for an installation whose media lives on another origin (ADR-0047).
security.md §9 specifies a `Content-Security-Policy` for the media origin and for nothing else, because until now nothing served HTML. The API's own policy - `default-src 'none'` - is right for a document that is never a document and fatal for one that is: under it a page may load no script, no stylesheet and no font. So the UI gets its own, and the API keeps its own unchanged.
Every source is 'self'. That is the dividend of embedding: the bundle and the API come from one origin, so nothing needs a foreign one, and no fetch, font or image has anywhere else to go. There is deliberately no 'unsafe-inline' and no 'unsafe-eval' in it - which makes this a constraint on the frontend framework that has not been chosen yet, rather than a consequence of one that has.
Variables ¶
This section is empty.
Functions ¶
func FS ¶
FS is the bundle rooted at the directory the files actually live in, so that a request for `/assets/x.js` is looked up as `assets/x.js` rather than as `dist/assets/x.js`.
func IsPlaceholder ¶
IsPlaceholder reports whether the binary was built without a real UI bundle.
The marker is the absence of the asset directory every build produces, not a magic string in the HTML: a bundle is a bundle because it has assets, and a check that reads the page would break the moment somebody edits a sentence in the placeholder.
func PolicyFor ¶
PolicyFor is the interface's policy for an installation whose media lives at mediaOrigin (ADR-0047): the ADR-0028 policy with that one origin added to `connect-src` - the browser has to PUT the bytes of an upload to a presigned URL there - and to `img-src` - a cover is drawn from a presigned URL there - and to nothing else. An empty origin is the `local` installation, whose media URLs are already 'self', and answers ContentSecurityPolicy byte for byte; a test holds it there, which is what keeps this from quietly widening the default installation.
The origin is trusted as given: scheme, host and port, derived by the composition root from the storage configuration it parsed for the adapter, and never from anything a request carries. Exactly one, never a list and never a wildcard - an installation has one storage configuration.
Types ¶
type Handler ¶
type Handler struct {
// Files is the bundle, rooted at its own directory.
Files fs.FS
// SecurityHeaders writes the common header set. Required; nil means a UI served without the
// headers of security.md §9, which is not a configuration this project offers.
SecurityHeaders SecurityHeaderWriter
// contains filtered or unexported fields
}
Handler serves the embedded bundle.
Everything it answers is a static file, so it needs no actor, no tenant and no transaction, and it sits outside the API's middleware chain: a page load fetches half a dozen assets, and spending the anonymous rate limit budget on them would make the first visit the last one.
func NewHandler ¶
NewHandler reads the bundle once and computes an entity tag per file, and composes the policy once from the installation's media origin - empty for an installation whose media is this server's own, the storage endpoint's origin for one on object storage (ADR-0047).
The tags are what make `no-cache` on the document cheap. Without a validator, "always revalidate" degenerates into "always re-download": an embedded file has no modification time - every byte of it was fixed when the binary was linked - so there would be nothing for the browser to ask about. With one, a reload that changes nothing costs a 304.
type SecurityHeaderWriter ¶
SecurityHeaderWriter writes the response header set of security.md §9 with the given content security policy.
It is an interface rather than a direct call into presentation/rest so that this adapter does not depend on that one; the composition root passes rest's implementation, and the header set keeps being defined in exactly one place.