~bigbes/sr-ht-spec

c945f3829a62025b9432a57b1d87a2c93d7b2aa8 — bigbes 27 days ago be096fc
docs: add SourceHut integration section to the spec.sr.ht design

Records the config-driven wiring (Recipe B: pure Go, chrome copied from
compare.sr.ht), the canonical [spec.sr.ht] keys, and the shared keys read
in place.

Documents why skipping GraphQL federation is safe: api.sr.ht federates
every .sr.ht config section with no allow-list, but updateSchema skips
services that fail to serve /query, and the refresh is SIGHUP-driven
rather than a ticker.
1 files changed, 86 insertions(+), 1 deletions(-)

M docs/DESIGN.md
M docs/DESIGN.md => docs/DESIGN.md +86 -1
@@ 294,6 294,87 @@ consume this: `spec_search`, `spec_read`, `spec_propose`, `spec_comment`,
Genuinely net-new: proposals, prose diff, review UI, agent tokens, frontmatter
lifecycle, projects/aggregation.

## SourceHut integration

**Recipe B** from the `sourcehut-custom-service` model: pure Go, API side on
`core-go`, web chrome reimplemented (`core-go` has no HTML templating — the
Jinja chrome lives only in Python `core.sr.ht`). compare.sr.ht already did
exactly this, so its nav template, cookie→identity handler, and SCSS entry are
the thing to copy rather than rederive.

Integration is entirely config + nginx + DNS. **No upstream source is modified.**

### Our section

The section name **must** be the literal `spec.sr.ht` — the `.sr.ht` suffix is
what puts us in the nav `network` list (`core.sr.ht/srht/app/flask.py::_network`)
and what other services look us up by.

```ini
[spec.sr.ht]
origin=https://spec.srht.bigb.es
connection-string=postgresql://specsrht@localhost/spec.sr.ht?sslmode=disable
; Bare repos, service-owned: <repos>/~<user>/<space>
repos=/var/lib/spec
; Materialized checkouts + bleve project indexes. Pure cache; safe to delete.
cache=/var/cache/spec
static-dir=/usr/share/sourcehut/spec.sr.ht/static
;bind-address=127.0.0.1:5091
migrate-on-upgrade=yes
```

Canonical key names only — `origin`, `connection-string`, `migrate-on-upgrade`
are read by the shared accessors (`config.GetOrigin`, `server.WithDefaultMiddleware`).
`repos` mirrors dolt.sr.ht/git.sr.ht; `static-dir` and `bind-address` are the
local house convention already used by both siblings. `cache` is ours.

### Shared keys we read in place (never duplicate)

| Key | Used for |
|---|---|
| `[sr.ht] network-key` | Fernet-decrypt `sr.ht.unified-login.v1`; mint Internal auth tokens |
| `[sr.ht] owner-name` / `owner-email` | **`config.GetOwner` panics if missing**; also the committer identity on merges |
| `[sr.ht] site-name` / `environment` | nav brand; non-`production` shows the dev banner |
| `[sr.ht] internal-ipnet` | this host must fall inside it or internal GraphQL calls are rejected |
| `[webhooks] private-key` | **`crypto.InitCrypto` fatally requires it even though v1 emits no webhooks** |
| `[meta.sr.ht] origin` | login/logout redirects, profile fetch, PAT validation |
| `[git.sr.ht] repos` / `api-origin` | only for read-only mounts of `docs/` dirs in existing git.sr.ht repos |

### Wiring checklist

1. **Config** — the `[spec.sr.ht] origin=` line must be visible to **every other
   service's** config, not just ours; each service builds its own nav
   independently. One shared `/etc/sr.ht/config.ini` makes this automatic —
   then **restart the other services** so they pick up the switcher entry.
2. **DNS** — `spec.srht.bigb.es` must be under the shared cookie domain
   (`*.srht.bigb.es`), or the unified-login cookie is never sent to us and every
   viewer looks anonymous.
3. **nginx** — plain `proxy_pass http://127.0.0.1:5091`, modeled on
   `contrib/compare.sr.ht.conf`. Note `client_max_body_size` needs raising if
   attachments are allowed.
4. **internal-ipnet** — same prerequisite as both siblings.
5. **Migrations** — `specsrht-migrate`, a brant wrapper, copied from
   `doltsrht-migrate`.

### GraphQL federation: deliberately not in v1

Bots use REST + MCP; a gqlgen schema buys nothing for the loop this service
exists to serve. dolt.sr.ht made the same call.

**Verified that opting out is safe.** `api.sr.ht` federates *every* config
section ending in `.sr.ht` — it does not consult an allow-list, so our section
does get added as a federated service pointed at `origin + "/query"`, which we
will not serve. `updateSchema` (`api.sr.ht/main.go`) handles this explicitly:
services that are offline or have an invalid schema are logged and **skipped**,
and `BuildSchema` runs over only the healthy ones. So the effect is one
`Unable to update service "spec.sr.ht"` log line, not a broken gateway.

It is also not a hot loop: schema refresh is **SIGHUP-driven, not on a ticker**
(the goroutine selects on `signalChan`), so the failing fetch happens at startup
and on explicit reload only. dolt.sr.ht and compare.sr.ht already run with this
exact property today. Should we ever want federation, adding `api-origin=` and
serving `/query` is the whole change.

## Phases

**Phase 0 — de-risk gate.** Two spikes, both must pass before anything else is


@@ 331,6 412,10 @@ notifications, vector search over the project index.
- **Project URL namespace.** `~user/+project` distinguishes projects from spaces
  (`~user/space`) in one character; alternatives are `/projects/~user/name` or
  reusing hub.sr.ht's own namespace.
- **Port.** compare.sr.ht is on 5090, dolt.sr.ht on 5306–5308. 5091 is free.
- **Port.** compare.sr.ht is on 5090, dolt.sr.ht on 5306–5308. 5091 is free and
  is what the config block above assumes.
- **MCP transport.** Streamable HTTP on the same chi router (`/mcp`) keeps it to
  one listener and one nginx block; a second port is only needed if MCP ends up
  wanting different timeouts than the web UI.
- **LICENSE.** Unchosen, same as compare.sr.ht. SourceHut's own services are
  AGPL/GPL.