~bigbes/sr-ht-spec

ref: 3cb1c03d8078d5748cc13a2e9bd7ba7d078e1b37 sr-ht-spec/go.sum -rw-r--r-- 38.9 KiB
3cb1c03d — Eugene Blikh 2 days ago
go.mod: take the shared libraries' current heads

ecore for mcphttp and the hot-path work, core for the errors fix: its seven
gqlerror sentinels were package-level pointers that gqlgen stamped with the
first field path they ever took, which was a wrong answer, a cross-request
leak and a data race.
5bb0bb13 — Eugene Blikh 2 days ago
graph: serve /query on the anonymous router with a bearer credential

The schema was mounted by core-go's server.WithSchema, on the authenticated
router, behind an ownerOnly middleware. That put it on meta.sr.ht's OAuth
vocabulary while every other surface of this service — the web UI, /mcp, the
REST write plane — authenticates with a tokens.sr.ht working token, so a
credential that reads through /mcp was refused by the endpoint meant to be the
instance-native read plane. dolt.sr.ht's graph package is the pattern; this
follows it.

/query is now mounted on the anonymous router and graph.Server installs its own
credential middleware: a working token owned by [sr.ht] owner-name and carrying
spec:read reads, one without that grant is 403, one belonging to anybody else is
403, and anything that does not verify is 401 with the bearer challenge. A
cookie is not a credential here — the principal is overwritten with the
anonymous one when no bearer token is presented, so no middleware above the
mount point can promote a browser session into read authority.

ownerOnly's rule survives the move: it compared auth.AuthContext.Username to the
owner, and authn's resolver refuses a foreign token's owner at the door with the
same 403. What it also did — remapping the owner to AUTH_INTERNAL so core-go's
webhook engine would accept them — is now coreauth's, which is what that package
was written for and had no caller for until today.

A service that mounts its own /query owes the instance api-meta.json, because
core-go serves that file only for the schemas it hosts itself. sr-ht-ecore's
apimeta serves it, with an empty scope list: spec.sr.ht defines no meta OAuth
scope and no @access directive to check one against, and a JSON null there is a
500 on meta's personal-token page for the whole instance.

Two consequences worth naming.

A meta.sr.ht personal access token no longer reaches /query. It did while
core-go's auth.Middleware stood in front of it. Accepting one again means giving
spec.sr.ht a meta scope first, and there is none to invent.

WebhookSubscription.sample cannot be rendered on this endpoint and says so.
corewebhooks.Exec reads the complexity bound off core-go's server context, which
only WithDefaultMiddleware installs and which cannot be built from outside that
package. Delivery is unaffected: the queue's context comes from WithQueues and
does carry it — but MaxComplexity must now be set by hand, because zero there
fails every delivery rather than imposing no limit.
9cec0f54 — Eugene Blikh 9 days ago
deps: tidy after the third uplift
ed79a1bb — Eugene Blikh 9 days ago
deps: the ecore packages of the third uplift
cd1b8b01 — Eugene Blikh 9 days ago
deps: auxilia whose scribe.Err reads the whole error chain

Until now scribe.Err type-asserted the outermost error for slog.LogValuer,
so a culpa error under a single fmt.Errorf logged as a flat string and lost
its code, hint and stacktrace with no warning. It walks the chain now, which
is what makes the culpa wrapping in this service visible in the journal.
3d811988 — Eugene Blikh 9 days ago
service: wrap the merge and reject failures with culpa

Three sites, all of them the outermost wrap on an error that becomes a 500:
the merge path's non-staleness arm, the reject path's default arm, and the
merged-but-the-row-did-not case, whose remedy now rides on the error as a hint
instead of sitting in a sentence one wrap away from being buried.

Outermost is the whole criterion. scribe.Err type-asserts the error it is
handed for slog.LogValuer rather than searching the chain, so a culpa error with
an fmt.Errorf above it logs as a plain string and the stacktrace is lost — which
is also why the sentinel arms stay fmt.Errorf: they carry no cause worth a
stack, and their text is what a 404 or 409 shows a viewer.

errors.Is and errors.As traverse culpa's wrap, so the sentinel mapping in the
surfaces is unchanged.
3a56ee61 — Eugene Blikh 9 days ago
deps: bump sr-ht-ecore for the slog panic reporter

middleware now reports a recovered panic through log/slog with method, path,
panic and stack as fields instead of one stdlib log line. It logs through the
default logger, which is the next commit's business.
e756d504 — Eugene Blikh 9 days ago
web: draw the whole web tier from sr-ht-ecore

The chrome moved last time; this moves the four packages around it. pages
discovers the page templates and carries the shared error body, so the
hand-maintained page list, the render helper and error.html go; assets finds the
hashed stylesheet and serves the static tree, so the local regexp, the href glob
and handleStatic go; middleware brings the private-cache policy and a panic
guard that aborts a connection rather than appending an error page to a
truncated one; and ecoretest replaces the hand-built config and the TestMain
that minted its own keys.

The one that matters is csrf. The guard was a predicate three handlers
remembered to call, which made unprotected the default for any form added
later. It is now csrf.Require on the router, so it covers the routes that are
not written yet, it runs before routing — an unrouted POST is refused rather
than 404'd — and the refusal is the shared sentence. Host comparison becomes
case-insensitive, which is strictly more permissive and only for an operator
typo in the config.

threads.html becomes _threads.html, the partial spelling pages.Load discovers,
and the review page takes container-fluid: two prose columns do not fit the
centred container, which is what cover and dolt already concluded for their wide
views.

What stays here: the sentinel-to-status mapping in fail, and a renderError that
wraps ecore's error body in this service's view struct.
c7477607 — Eugene Blikh 9 days ago
authn: accept tokens.sr.ht working tokens beside the agent token

A second agent credential plane, next to the existing one rather than in
place of it. The agent_token table, every agent configured with it, and
the refs rule and provenance requirement around it are untouched; the
local plane is removed in a later phase, not this one.

The resolver tries the instance plane first and falls back to the local
store on exactly two refusals, bearer.ErrInvalid and bearer.ErrNotOurs.
spec's local token has no prefix to discriminate on — it is 32 random
bytes in base64, which is precisely what "did not decode as one of ours"
looks like — so the fallback replaces the shape test bench and cover can
afford. ErrRevoked, ErrForbidden and ErrUnavailable are terminal: a
withdrawn credential must not get a second chance at the old door, and an
unreachable daemon must not silently degrade into the legacy plane.

Grants ride on the principal and are checked where the action is known,
never in the middleware, which runs upstream of the router: spec:propose
in service.Propose, below both write surfaces, and spec:read in each read
surface's gate. /mcp checks per tool rather than at its Gate, because one
endpoint carries both kinds and a surface-wide read grant would refuse a
propose-only token at initialize. Principal.Authorize is a no-op off the
instance plane, which is what keeps the local token working.

The instance plane brings an owner where the local token had none, so a
working token belonging to anybody but [sr.ht] owner-name is refused
rather than admitted as a second identity: Principal.Owner is read by the
provenance committer, the refs rule's principal kind and the coreauth
AuthContext, all written for one human.

StatusFor is the one status table. ErrUnavailable is 503 and never 401 —
reading "I could not ask tokens.sr.ht" as "revoked" would refuse every
live instance token while a daemon that is deliberately off the hot path
restarts.

An instance with no [tokens.sr.ht] section builds no instance plane and
starts anyway, serving its own agent token as before.
8374a1ef — Eugene Blikh 25 days ago
feat(graph): GraphQL-native webhook surface (Phase 5a)

The webhook types, mutations, and resolvers, adapted from the pages.sr.ht
core-go template for spec's single-owner model.

- SDL: WebhookEvent (PROPOSAL_OPENED/MERGED/REJECTED), WebhookSubscription
  interface + UserWebhookSubscription, WebhookDelivery, WebhookPayload
  interface + ProposalEvent (carries a Proposal), cursor wrappers,
  `webhook` payload root field, and a `type Mutation` with
  createUserWebhook / deleteUserWebhook. No OAuth `client` field and no
  @access/@private directives — spec has no OAuth clients or scopes, so
  the owner gate is the entire ACL.
- Models: hand-written database.Model impls (UserWebhookSubscription,
  WebhookDelivery) so gqlgen autobinds rather than generates them; events
  via pq.Array; cursor keyset pagination.
- Resolvers: all owner-gated via authn (spec's ACL), using core-go's
  webhook engine — Validate, NewAuthConfig (INTERNAL, via the coreauth
  bridge), FilterWebhooks, WebhookContext.Exec for the sample, and the
  `webhook`→Payload(ctx) root. Proposal writes deliberately stay off this
  surface (only webhook mutations; the schema test now asserts exactly
  that).
- gqlgen.yml binds Cursor to core-go's model.Cursor; generated code
  regenerated with the pinned gqlgen v0.17.36 (reproducible).

Compiles and vets clean; existing graph read tests still pass. Runtime
context wiring and event firing are the next slices.
5eab0915 — bigbes 27 days ago
chore: promote fernet-go and go-ini to direct dependencies

authn/ imports both directly to decrypt the unified-login cookie and read
the instance ini, so the toolchain reclassifies them. No version changed
and no module was added.

Deliberately not running go mod tidy yet: bleve, chi, brant and the MCP
SDK have no importer until phases 2 and 3, and tidy would drop them from
go.mod, reintroducing it into every later parallel wave's file set.
33a3deaf — Eugene Blikh 27 days ago
feat: foundation — go.mod with every dependency, and the core/ domain

Phase 1 foundation commit. Two things, so that later parallel waves write
disjoint directories and never touch go.mod:

  - go.mod / go.sum carrying every external dependency the whole module will
    need (go-git, bleve, goldmark, chi, lib/pq, yaml.v3, the MCP SDK, brant,
    auxilia, testify, and the sr-ht-core fork). Populated by building a
    throwaway blank-import file, which is then deleted; `go mod tidy` runs
    once, at the very end of the build-out.
  - core/, the pure domain: owner/space names, safe relative paths, the
    globally-unique document ID grammar, frontmatter parsing and schema
    validation, `.spec.yml` policy with auto_merge glob matching, and the
    proposal state machine. Standard library plus yaml.v3, nothing else.

Two design invariants are enforced here rather than documented and hoped for:
"approved" is not a status (it is a property of the branch a document is
reachable from), and the proposal machine has exactly open/merged/rejected.
A per-space `.spec.yml` cannot reintroduce either.

Note on the sr-ht-core pin: the design calls for a `replace` onto
git.srht.bigb.es/~bigbes/core-go at c2c2f38, but that commit's go.mod still
declares `module git.sr.ht/~sircmpwn/core-go`, so Go rejects the replacement.
Both siblings pin the later dd418a20 under the canonical path with no
replace; this does the same.