~bigbes/sr-ht-ecore

001f23a2 — Eugene Blikh 9 days ago
login: the one decoder of the unified-login cookie

Six services decode sr.ht.unified-login.v1 independently, and each of them
makes the same five decisions: DecryptWithoutExpiration rather than a decrypt
with a service-side TTL, unmarshal into auth.AuthCookie, strip the leading '~',
treat every failure as anonymity rather than an error, and validate the name
before it reaches a path, a log line or a SQL parameter. Two of the six had
already dropped the last one.

The middleware is split into Optional and Required because that is where the
copies diverged: every donor folded its own gating policy into a single
middleware — never refuse (public clones), never 401 (visibility decided
downstream), always send the viewer to meta's login page — so one middleware
here would force most services to write the other half again.

ValidName ships as the default rather than being required: the grammar is
meta.sr.ht's and therefore instance-wide, and requiring a validator would put
the safe path behind an extra argument that the services validating nothing
today would go on not passing. WithValidator narrows it; a nil validator
restores the default, so "accept anything" has no spelling in the API.

Mirroring meta's profile into a service's own user row stays in each service:
that touches its own schema.
ed3a5af7 — Eugene Blikh 9 days ago
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.
66c00770 — Eugene Blikh 9 days ago
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.
3310ac7e — Eugene Blikh 9 days ago
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.
350dcb0b — Eugene Blikh 9 days ago
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.
f019dbe4 — Eugene Blikh 9 days ago
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.
9377c43a — Eugene Blikh 9 days ago
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.
6a2cf04c — Eugene Blikh 9 days ago
README: the web-tier packages
996577de — Eugene Blikh 9 days ago
pages: the shared page set, buffered render and error page
77b1a8d2 — Eugene Blikh 9 days ago
middleware: the shared cache and panic handling
12ae8522 — Eugene Blikh 9 days ago
ecoretest: the shared instance config and crypto bootstrap for tests
ede9467a — Eugene Blikh 9 days ago
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.
4d7f4c4a — Eugene Blikh 9 days ago
assets: the shared hashed-asset discovery and cache policy
8c8a1ce2 — Eugene Blikh 9 days ago
gitignore the in-repo git worktree directories
2fd6b3aa — Eugene Blikh 9 days ago
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.
17411599 — Eugene Blikh 10 days ago
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.
86de8532 — Eugene Blikh 10 days ago
bearer: the shared working-token validator of SPEC ch. 6
5ba967ed — Eugene Blikh 10 days ago
grants: the shared grant vocabulary of tokens.sr.ht
36315856 — Eugene Blikh 10 days ago
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.
0dfd1e5d — Eugene Blikh 11 days ago
ci: gitsync mirror webhook for the srht mirror
Next