From 6a2cf04c584fc38cab79188710450550ab9a5210 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Sat, 8 Aug 2026 22:19:13 +0300 Subject: [PATCH] README: the web-tier packages --- README.md | 89 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/README.md b/README.md index 97f7e563861cdadd65723ad3920e2d7e606e66f6..9a7e58a4a6affea1a3d9ade73227b4ae46555a22 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,49 @@ copies of the same code. registered token — ask tokens.sr.ht whether it is still live, behind a 60s cache. Every step that can refuse locally runs before the one that cannot, so a short token never touches the network at all. +- `pages` — the page-template machinery: discover a service's pages from its + embedded tree, refuse at startup a page that defines no `content`, render + into a buffer before touching the response, and carry the shared error page. + Both invariants are the point. A content-less page would serve the chrome + around a hole with a 200, which is why `content` is invoked with + `{{template}}` and must never become a `{{block}}` — a block quietly defines + an empty default for every page at once. And a template that fails halfway + has already written a partial page unless it rendered into a buffer, at + which point the error page cannot be rendered through the call that just + failed. `Render` answers the response itself and returns the error only for + the log, because handing it to a service's `fail` would either overwrite a + committed response or recurse through the page that just broke. +- `assets` — hashed static assets: find the current `main.min..css` in an + `fs.FS` (an `embed.FS` or an `os.DirFS`, since the services use both), and + serve the static tree with the cache policy the hash implies — `immutable` + for a content-addressed name, an hour for everything else — without + publishing a directory listing. An absent asset resolves to `""` rather than + an error: a service that will not boot without a build artefact cannot be + run from a checkout. The empty href must then be guarded in the template, + because `` re-requests the page it is on. +- `csrf` — the same-origin guard. These services have no CSRF token: the + session cookie is meta.sr.ht's unified-login cookie, set on the parent + domain, and no individual service can set its `SameSite`. So the defence is + an Origin/Referer check against the service's own origin, and it belongs + here for the same reason `grants` does — one rule, five copies, applied + inconsistently. A request that carries neither header is refused: one that + will not say where it came from cannot be shown to have come from us. +- `middleware` — the small HTTP middleware the services were copying between + each other verbatim: `private, no-store` for anything behind the login + cookie (`no-cache` still permits a stored copy, revalidated against the + *next* viewer's cookie), panic recovery through a service-supplied error + renderer, and nginx's 499 for a client that hung up — which a service wants + distinguished from a real failure, or a disconnected browser inflates the + 5xx rate. A panic that arrives after the response has started aborts the + connection instead of appending an error page to a truncated one. +- `ecoretest` — the test bootstrap: a synthetic instance `config.ini` with the + sections the nav rules need (canonical services, hub, the excluded + paste/pages, the custom ones, and one section with no origin that must not + appear in a switcher) and the fernet/webhook key seeding every service's + `TestMain` was doing by hand. Fixed keys, not generated ones — they secure + nothing inside a test process, and constancy is what lets two packages of + one service both initialise without the second rotating what the first + sealed with. ## Usage: chrome @@ -87,6 +130,52 @@ foreign bearer token is a meta PAT to accept (dolt) or something to refuse unreachable daemon as "revoked" would refuse live tokens across the instance for the length of a tokens.sr.ht restart. +## Usage: the web tier + +The five web-side packages are independent — adopt them one at a time — but a +service's `New` ends up reading roughly like this: + +```go +//go:embed static templates +var webFS embed.FS + +cssHref, err := assets.Resolve(webFS, "static/main.min.*.css", assets.DefaultPrefix) +// err is a malformed glob, not a missing stylesheet: absent resolves to "". + +svc := chrome.NewService(conf, "compare.sr.ht") +svc.StyleHref = cssHref + +set, err := pages.Load(webFS, pages.Options{Funcs: myHelpers}) +// Fatal at startup: a page that defines no "content" is a 200 around a hole. + +r.Use(middleware.PrivateCache) +r.Use(middleware.RecoverPanics(func(w http.ResponseWriter, r *http.Request, _ any) { + renderError(w, r, http.StatusInternalServerError) +})) +r.Use(csrf.Require(svc.SelfOrigin(), nil)) // nil deny = plain 403 with csrf.Message +r.Mount(assets.DefaultPrefix, assets.Handler(webFS, assets.DefaultPrefix, notFound)) +``` + +and a handler renders through the set, with the view struct embedding the +chrome: + +```go +vd := viewData{Page: svc.Page(r, "Title", username), Data: payload} +if err := set.Render(w, http.StatusOK, "index", vd); err != nil { + log.Printf("render index: %v", err) // the response is already answered +} +``` + +In tests, `ecoretest` replaces the hand-built config and the `TestMain` that +seeded crypto: + +```go +func TestMain(m *testing.M) { ecoretest.InitCrypto(); os.Exit(m.Run()) } + +conf := ecoretest.Config("compare.sr.ht") +noHub := ecoretest.Config("compare.sr.ht", ecoretest.Delete("hub.sr.ht")) +``` + ## Policy: chrome The chrome bakes in the instance-wide decisions instead of parameterizing