bearer: silence the std logger the benchmarks were timing
sr-ht-core's auth.DecodeBearerToken calls log.Printf on every token it
refuses, so BenchmarkValidate/invalid wrote one line per iteration. The cost
was not the file: log.Printf takes a mutex, and the case reported 925 ns/op
where the work is 92.5.
The file was a hazard too. An unfiltered run of this package produced 9.5
million lines and 901 MB, and benchfmt skips what it cannot parse — so
bench.sr.ht would have accepted that upload and reported success over it.
Ignore cover.out and bench.txt: make cover and make bench write them into
the checkout.
ci: test, coverage and benchmarks on builds.sr.ht
This repository had no CI while seven services pinned it, so a break here
was discovered in whichever service next ran `go get -u`. The manifest is
the sibling services' pipeline with everything only a service needs taken
out: no scss, no keygen, no abuild, no publish, no publish_artifacts, and
no postgres — nothing in this module imports database/sql, no test opens a
database and no test skips on a missing DSN. What is left is the Go cache,
the gates, the two runs and the two uploads to cov.srht.bigb.es and
bench.srht.bigb.es.
The Makefile is what the manifest calls, so the Go command lines exist
once. Its bench target filters its own output, and that is not tidiness:
`go test` merges the test binary's stderr into stdout, bearer's
invalid-token benchmark makes sr-ht-core's auth.DecodeBearerToken log once
per iteration, and an unfiltered BENCH_COUNT=10 run was 901 MB and 9.5
million lines deep — around the 290 result lines it produces — when it was
stopped. benchfmt ignores what it cannot parse, so that file would have
uploaded "successfully". Filtered, the same run is 340 lines and 31 KB.
The cure belongs in the benchmark, which should silence the logger the way
middleware's discardLog already does; the filter is the pipeline's defence
either way.
check-fmt and not gofmt, because `gofmt -l` prints the offending files and
still exits 0; cover refuses an empty profile and the bench task greps for
one benchmark name per benchmark file, because both would otherwise upload
nothing and call it a success.
benchmarks for the hot path
Twenty-nine of them, over what every request of every service actually
touches: working-token validation, grant parsing and matching, the chi and
net/http middleware, and the same-origin guard.
They exist to be uploaded. This library had no CI and no benchmarks at all
while seven services pin it, so nothing measured a regression here.
mcphttp: the MCP endpoint plumbing both services already share
hostAllowed is byte-identical in cov and bench, and neither should own it.
PrivateCache is cov's alone today, which is why bench's /mcp currently sets
no Cache-Control and no Vary at all. StreamableOptions carries the pair both
pass and the reason for it.
HostGuard takes cov's fail-closed stance: an origin with no host to guard
with is a construction error, not a warning. bench serves unguarded in that
case and has a test pinning it; reconciling that is bench's to do.
Deliberately not a shared MCP framework. Once the go-sdk and the per-service
tools are subtracted, this is the whole overlap.
chrome: give a service its own navigation row
The switcher lists the instance's services, each on an origin of its own.
Three artifacts pages rode ExtraNav into it, so that row's membership
changed from service to service and its active element - spent naming
"artifacts" - said nothing about which of the three the reader was on.
Sections is the second row instead, the one upstream draws as
.header-tabbed wrapping .nav.nav-tabs and meta puts profile and keys in.
A service declares its sections at startup; Page marks the one the
request's path stands in, so a page cannot forget to declare itself, and
a path in no section lights nothing rather than the first tab. Both
classes arrive with core.sr.ht's nav.scss, so the row costs no service
any CSS.
ExtraNav is deprecated with it: bench and cov abandoned it when the
instance deployed a tokens.sr.ht and the word appeared in the navbar
twice, and artifacts was its last user.
apimeta: serve the api-meta.json a self-mounted /query owes the instance
core-go serves this file for a service assembled through WithSchema, which
mounts /query on the authenticated router. A service whose API must answer
anonymous callers cannot use that — core-go's auth middleware 401s an
un-cookied request — so it mounts /query itself, and then nothing serves the
file meta.sr.ht reads to build its personal-token page.
The scope list is variadic and marshals empty rather than null, which is the
whole reason this is a package and not four lines per service: meta iterates
every discovered service's scopes on ONE page, so a single `"scopes": null`
is a 500 on /oauth2/personal-token for the entire instance rather than one
degraded entry — a failure nobody would find by testing the service that
caused it.
bd: turn on dolt auto-push
The debounce is 30s rather than the 5m default so a memory or issue written
mid-session reaches dolt.srht.bigb.es without waiting for the session to end,
and the 60s timeout keeps an unreachable remote from hanging bd.
bd init: initialize beads issue tracking
follow cover.sr.ht's rename to cov.sr.ht
The section in ecoretest's synthetic config.ini, and the grant vocabulary the
grants package uses for its examples — cover:upload was the stock example of a
well-formed grant, and the service that defines it now spells it cov:upload.
follow compare.sr.ht's rename to diff.sr.ht
The service renamed itself; what reaches this repository is the section name
in ecoretest's synthetic config.ini, which every service's switcher test is
built from, and the examples in the chrome docs and README that spelled the
old section. The fixture's custom services now read bench, cover, diff, dolt,
spec, tokens — alphabetical, which is the order BuildNav puts them in.
bearer: say what IsRefusal does not answer for
A service whose own refusals do not wrap these sentinels sends its "bad
token" into the else branch of the guard the doc suggests, and answers 503 to
a caller whose credential really was the problem. One of the six hit this while
adopting it.
ecore: the gaps the third adoption pass found
Five services adopted the new packages today and each hit the same edges.
bearer.IsRefusal: a service's resolver returns more than this package's
vocabulary — the Postgres lookup it had to make, an expired context, a bug —
and StatusFor's default answers those 401, which tells a caller to re-mint a
token that was never the problem. Every service that wrapped the table spelled
out the sentinel list to guard the delegation; now it does not have to.
instconf.Key.Because: three services kept their own hand-written config
checker rather than lose the sentence that says WHY a key is required
("crypto.InitCrypto exits without it"). Require reported the names and nothing
an operator could act on, so nobody used it.
logging.DefaultsWithoutDebugFlag: a migration CLI passes -d to brant, where it
means --dialect and takes a value, so `coversrht-migrate -d postgres` arrived
as a request for debug logging — silently, because the probe sees the flag and
never the value.
assets.DirFS: os.DirFS("") does not mean "no assets", it means "serve the
filesystem root", and it is one unset config key away. dolt wrote the guard;
the other five would each have to see it happen first.
pages.APIMessage: the REST half of every service keeps its 404 body
byte-identical on purpose, so it could not adopt the shared refusal installer,
whose messages are page sentences. The same table now answers in both
registers.
bearer: mint the internal authorization through internalauth
This package assembled the header by hand — json.Marshal of an auth.InternalAuth
plus "Internal " and a fernet seal — two files away from the package whose whole
purpose is to hold both ends of that handshake. It is the same drift internalauth
was hoisted to end, sitting inside the library that hoisted it: a change to the
payload shape here would have gone unnoticed by every Guard on the instance until
a revocation check started failing.
README: the packages of the third uplift
chimw: the chi-shaped helpers of the custom services
GetHead registers a read route under GET and HEAD both, which cover, bench
and tokens had each spelled out — five copies of three lines, under three
names. RenderRefusals points chi's NotFound and MethodNotAllowed at the
service's own error page, which spec and dolt install neither of, so a
mistyped URL there is the one refusal on the instance that answers in
plain text. RequestLogger is a chi LogFormatter emitting slog records, in
place of chi's colourised stdlib line on stdout — the highest-volume line
a service writes and the only one that was neither structured nor on
stderr.
It is a second middleware package and not a wing of the first because
sr-ht-ecore/middleware needs nothing but net/http and that rule is worth
keeping; the boundary is whether the helper has to know what a route is.
A request line is Info, a 5xx is Error, and 499 stays Info by being below
500, which is what that code was chosen for. Skipping probe noise is the
caller's call, through a predicate. The logger goes outside RecoverPanics
so the line reports the error page that was rendered rather than the
nothing an unwinding stack has written.
internalauth: both ends of the Internal authorization
core-go implements this check unexported inside auth.Middleware, so a
service that wants the guard without the session resolution behind it
writes it again: dolt.sr.ht did, in web/handlers_internal.go, and the
caller minting the header for it is a different program in the same
repository (cmd/dolt-git-hook) that spells the payload out by hand.
Copies drifting is the smaller half. The two ends of one protocol shared
no type, no constant and no test, so a change to the payload shape, the
scheme or the expiry breaks nothing at build time and stops provisioning
at the next push. They are two functions over one struct here.
The check stays core-go's: source address inside [sr.ht]internal-ipnet
AND a fernet token sealed with [sr.ht]network-key, at most 30s old, both
required. Three deliberate departures. The caller may be pinned to a
client and node id, which upstream only checks are non-empty; a payload
that decrypts but is not an internal auth is a 403 rather than a panic,
as is a RemoteAddr that does not parse; and the refusals are sentinels,
so "not internal at all" is distinguishable from "internal but wrong
client" without parsing an error string.
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.
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.