logging: the instance's slog policy, without the handler middleware.RecoverPanics reports panics through slog's default logger, so ecore already depends on every service installing a compatible handler and had no way to say so. A service that forgets slog.SetDefault prints its panic reports in Go's plain format, unlevelled and unmasked, into a journal where everything else is tinted. Six services (compare, spec, dolt, cover, bench, tokens) had each written the same level parser, the same os.Stderr.Stat colour probe, the same -d scan of os.Args, and their own copy of the credential mask list. The copies had drifted into three key sets and four patterns: only spec masked the config private keys, only tokens-migrate knew a DSN carries a password, and only dolt masked pubkey and credential. What is redacted — the unified-login cookie, tokens.sr.ht working tokens, the Authorization header they ride in — is a fact about the instance, so it is maintained once. Defaults() resolves the policy and Install() sets the default logger; the handler stays with the caller, because ecore is a small SourceHut library and auxilia is a large general one, and a service that wants a JSON handler for a log shipper should not link a tinting one to share a mask list. Options.ReplaceAttr applies the same masking through stdlib slog alone, so the split costs a non-scribe service nothing. tokens.sr.ht's partial mask is exported but not defaulted: six characters of a live working token is still six characters of a live working token. Level: -d, then $LOG_LEVEL, then [section]log-level, then info; an unreadable value falls through to the next source rather than refusing to boot.
instconf: one reading of the instance config origins Five services and this library each grew a copy of the same three lines that turn config.ini into an origin, and the copies drifted. Two donors canonicalized with TrimRight and TrimSuffix respectively, so a config carrying "https://x//" produced two different strings in two daemons that must produce the same one when comparing against an Origin header. One repo held two origin-to-host extractors that disagreed about a malformed origin: one returned an error, the other answered "localhost". A third copy feeds the DNS-rebinding guard of an MCP endpoint, where an empty host disables the guard. CanonicalOrigin takes the strict reading (TrimSpace, then every trailing slash). ExternalOrigin and InternalOrigin are two names rather than one GetOrigin with a bool, because a flipped flag is invisible until a browser is redirected to an address only the daemon can reach. OriginHost returns a host name and OriginAuthority a host[:port], the two things the disagreeing donors each needed, and neither invents a host for an origin that names none. InternalAPIOrigin walks the four-key ladder and reports absence as a bool instead of core-go's panic, and Require reports every missing key in one error so an operator fixes the config in one pass rather than one restart per key.
bearer, pages: the refusal table and the form read that must not drift bench wrote the bearer refusal switch three times in one repo — REST, MCP and its resolver — and each copy re-decides the arm that matters: ErrUnavailable is 503, never 401. Reading an unreachable token daemon as "revoked" tells every CI job on the instance its credential is bad for as long as tokens.sr.ht takes to restart. That warning has lived in this repo's README as prose, where it cannot be imported; StatusFor puts it in code, and its default sends an unrecognised failure to 401 so a forgotten arm refuses a request rather than declaring the service unwell. FormValues returns r.PostForm and never r.Form. r.Form merges the query string into the body's values, so a mutation could be driven from a URL somebody was linked to — the one request the same-origin guard cannot fault, because it did come from our own page. The difference between the safe version and the hole is one character in a field name, in a function every service with a form writes for itself.
chrome: a default favicon, optional listing columns, and a table partial Three services asked for a favicon field and bench argued against one: it ships no icon deliberately, and its layout says why — a <link rel="icon"> pointing at an asset the binary does not have is a 404 on every page load, for a file nobody asked for. A default that is a path hands that to every service without a logo, so the default carries its own bytes instead: the brand's ring as a data: URI, which cannot 404 and costs no request. It is a template.URL because html/template rewrites any href whose scheme is not http, https or mailto to "#ZgotmplZ" — the type is how a caller says it meant a data: URI. Updated and Meta are optional for the reason the four listing services could not agree: bench and spec have a modification time, dolt's schema has no timestamp at all, and cover's index is a table of sparklines no shared partial will render. A required column would have pushed dolt back onto a local copy. Updated is a time.Time so the partial renders "3 hours ago" with the exact stamp in the title once, rather than five services spelling it five ways. srht-repo-table is a second partial over the same dot rather than a variadic first one: growing columns on the cards would have made them worse cards for the services that wanted cards. Attach now installs Funcs itself, because the partials call reltime and abstime and an unknown function is a parse error — a caller who had not merged Funcs would have got a startup panic naming a template it never wrote. pages consequently attaches before layering the service's own map on top, which is what keeps a deliberate shadow working.
middleware: report panics through slog The two log.Printf lines were the last stdlib log in ecore, and they printed a formatted sentence where every service on the instance now emits structured records — a panic report that cannot be filtered by path or grouped by route is the one log line an operator most wants to query. Through slog's default logger rather than one handed to RecoverPanics: a library has no business choosing a handler. The service installs scribe's at startup with SetDefault, and these land in the same stream, with the same masking, as its own lines.
chrome: carry the service's extra hashed assets bench and cover each grew a pair of UplotCSSHref/UplotJSHref fields beside the embedded Page, compare a BundleHref, and all three wrote the same justification next to it: the hash in the name is a property of the binary rather than of a page, so a page handed its own asset URLs is one that can be written without them and silently render nothing where the chart was. That argument is about the chrome, so the slot belongs to the chrome. A map rather than named fields because ecore has no business knowing that this instance vendors uPlot; the service names its own artefacts and its layout reads them back with index, guarded on emptiness the way StyleHref is.
README: the web-tier packages
pages: the shared page set, buffered render and error page
middleware: the shared cache and panic handling
ecoretest: the shared instance config and crypto bootstrap for tests
csrf: the shared same-origin guard Five services carried their own copy of the Origin/Referer check and the copies had drifted, not only in wording: tokens, cover and bench apply it as router-wide middleware, while dolt and specs call a predicate from individual handlers, so a form added to either of those goes out unguarded. Require is therefore the API this package leads with, and SameOrigin is the escape hatch for a service migrating one route at a time.
assets: the shared hashed-asset discovery and cache policy
gitignore the in-repo git worktree directories
chrome: give the brand both halves, and share the login URL and time helpers The brand was upstream's minus its hub link: core.sr.ht points the whole brand at hub when the instance has one and drops the red service label, while this package kept the label and pointed at the service root. Hub is excluded from the switcher, so that left the chrome with no route to hub at all. Split the brand in two — the site name to hub (to the service root on an instance without one), the label to the service root — and neither half has to lose. The label keeps its own <span class="text-danger"> because the theme colours ".navbar-light .navbar-brand a", which outranks .text-danger and would repaint a red <a> white in dark mode. LoginURLFor exports the line Page already computed. A handler that gates a page behind login was building an entire Page to read one field off it. RelTime/AbsTime come up from the services, where the copies had already drifted over what to print for a future instant: "in 3 hours" on one service, "just now" on the next, for the same timestamp.
bearer: split the grant check out of validation A sourcehut service resolves identity once per request in middleware, upstream of the router — that is where the cookie plane and the bearer plane meet and where a principal is put on the context — and at that point nothing knows which route will run, so nothing knows the action. The action is known one layer down, in the handler. Validate insisted on both at once, which left every adopting service two bad options: invent an action before it has one, or lift its bearer plane out of the middleware every other plane goes through. The second is how a surface ends up with two different ideas of who is calling. So Inspect answers steps 1, 2 and 4 — who, what may they do, is it still live — and Token.Authorize is step 3, asked where the action finally exists. Validate is unchanged for callers that know both at one point. It keeps its own ordering rather than becoming Inspect+Authorize: step 3 before step 4 means a token that does not carry the grant is refused without a round trip to the daemon, and a test pins that. Inspect cannot keep that ordering, having no action to refuse on, and that is the one cost of the split — written down at both call sites.
bearer: the shared working-token validator of SPEC ch. 6
grants: the shared grant vocabulary of tokens.sr.ht
chrome: repo-list partial and fixed-width brand srht-repo-list renders project listings (repos, databases, spaces) as the family's event-list cards from a neutral ListItem shape, encoding the rules the services converged on: h4 title link, visibility as small muted lowercase text on the right for non-public entries only, description as a paragraph. The empty state is a parameter. The brand in srht-nav gains min-width: 15rem so the service switcher starts at the same x-coordinate on every service — without it the menu shifts by the width of the red service label when hopping between services.
ci: gitsync mirror webhook for the srht mirror
chrome: shared page chrome for the instance's custom services Extract the nav/chrome idiom that compare, spec, dolt and cover each carried as a private copy (and bench was about to fork as a sixth): BuildNav over the shared config.ini with the canonical ordering and paste/pages/hub exclusions, a per-request Page with login/logout/profile URLs against meta's unified login, embedded srht-nav / srht-env-banner partials (circle brand + red service label + switcher + login box), and the generic dict/shortsha template helpers. Policy decisions the copies had drifted on are baked in: switcher only for authenticated viewers, hub excluded in the builder rather than the template, profile link prefers hub's ~username page, environment name uppercased for the banner. Service-specific needs surfaced by the cover survey get seams instead of copies: ExtraNav for extra switcher entries, ContainerClass for full-bleed pages, the config section as an explicit parameter for the active check.