~bigbes/sr-ht-ecore

6a2cf04c584fc38cab79188710450550ab9a5210 — Eugene Blikh 9 days ago 996577d
README: the web-tier packages
1 files changed, 89 insertions(+), 0 deletions(-)

M README.md
M README.md => README.md +89 -0
@@ 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.<sha>.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 `<link href="">` 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