~bigbes/sourcehut-root

d9aac8a275e68ad733cd154140f496d8ff2309ae — Eugene Blikh 26 days ago 50df4c2 master
Make sourcehut-custom-service a global skill installed via Justfile

Move the skill from project-local .claude/skills to the skills/ backup dir
(matching sourcehut-ci) and generalize the Justfile install target to a skill
list, installing both into ~/.claude/skills. Replace absolute mirror paths with
repo::<path>::<symbol> references resolved against a single __SRHT_MIRROR__
placeholder that the install target substitutes with the repo's absolute path;
switch citations from drift-prone line numbers to stable symbol names and fix
a stale core-go/auth/internal.go reference to core-go/auth/middleware.go.
2 files changed, 75 insertions(+), 57 deletions(-)

M justfile
R {.claude/skills => skills}/sourcehut-custom-service/SKILL.md
M justfile => justfile +20 -7
@@ 1,12 1,25 @@
_default:
    @just --list

skill := "sourcehut-ci"
src   := "skills/" + skill
dst   := "~/.claude/skills/" + skill
# Skills backed up in this repo, installed into ~/.claude/skills (repo -> ~/.claude)
skills := "sourcehut-ci sourcehut-custom-service"

# Install the backed-up skill into ~/.claude/skills (repo -> ~/.claude)
# Absolute path to this repo (the SourceHut documentation mirror), substituted
# into skills for the __SRHT_MIRROR__ placeholder at install time.
mirror := justfile_directory()

# Install the backed-up skills into ~/.claude/skills
install:
    mkdir -p {{dst}}
    rsync -a --delete --exclude='.*' {{src}}/ {{dst}}/
    @echo "Installed {{skill}} -> {{dst}} ($(find {{dst}} -type f | wc -l | tr -d ' ') files)"
    #!/usr/bin/env bash
    set -euo pipefail
    for skill in {{skills}}; do
        src="skills/$skill"
        dst="$HOME/.claude/skills/$skill"
        mkdir -p "$dst"
        rsync -a --delete --exclude='.*' "$src/" "$dst/"
        # Substitute the mirror-root placeholder with this repo's absolute path
        find "$dst" -type f -name '*.md' -print0 | while IFS= read -r -d '' f; do
            sed "s|__SRHT_MIRROR__|{{mirror}}|g" "$f" > "$f.tmp" && mv "$f.tmp" "$f"
        done
        echo "Installed $skill -> $dst ($(find "$dst" -type f | wc -l | tr -d ' ') files)"
    done

R .claude/skills/sourcehut-custom-service/SKILL.md => skills/sourcehut-custom-service/SKILL.md +55 -50
@@ 11,7 11,13 @@ SourceHut has **no plugin system** — nothing loads third-party code into a run

**This only works on a self-hosted instance you control** (config + nginx + DNS), e.g. the user's `*.srht.bigb.es`. You cannot add a service to hosted sr.ht. SourceHut services are tightly-coupled siblings sharing `core.sr.ht`/`core-go`, **not** a versioned plugin API — when you bump those on a refresh, your service can break and it's on you to track it.

All file:line references below point into the read-only mirror at `~/data/home/sourcehut`.
### Reference notation

Citations are written **`repo::<path>::<symbol>`**: open `<path>` **relative to the mirror root** and search for `<symbol>` (a function, class, template anchor, or config section). Symbol names survive refreshes; line numbers don't, so they are deliberately omitted. The mirror root is the **single absolute path** in this skill, substituted at install time:

> **`__SRHT_MIRROR__`**

(In this workspace that is the repo holding the SourceHut documentation mirror. The `just install` target rewrites the token to the real path.)

---



@@ 21,14 27,14 @@ All file:line references below point into the read-only mirror at `~/data/home/s
The list of services in the top nav is computed from config — **every section name ending in `.sr.ht`**:

```python
# core.sr.ht/srht/app/flask.py:32
# repo::core.sr.ht/srht/app/flask.py::_network
_network = [
    s for s in config
    if s.endswith(".sr.ht") and s not in ["paste.sr.ht", "pages.sr.ht"]
]
```

The nav template loops over `network` and links each via `get_origin()`, which just reads `[service] origin=` from config (`core.sr.ht/srht/templates/nav.html:17-32`, `core.sr.ht/srht/config.py:67`).
The nav template loops over `network` and links each via `get_origin()`, which just reads `[service] origin=` from config (`repo::core.sr.ht/srht/templates/nav.html::for _site in network`, `repo::core.sr.ht/srht/config.py::get_origin`).

→ **Add a `[myservice.sr.ht]` section with `origin=` to each service's `config.ini` and your service appears in the nav of every SourceHut web UI.** (paste/pages are hardcoded out of the switcher; your service won't be.)



@@ 36,36 42,36 @@ The nav template loops over `network` and links each via `get_origin()`, which j
Services do **not** each run a separate web-login OAuth dance. There is a single shared cookie, `sr.ht.unified-login.v1`, set on the **global domain** (e.g. `.srht.bigb.es`), httponly, containing the user's profile as Fernet-encrypted JSON:

```python
# core.sr.ht/srht/app/flask.py:201-218  (read on every request)
# repo::core.sr.ht/srht/app/flask.py::get_session_cookie   (read on every request)
cookie = request.cookies.get("sr.ht.unified-login.v1")
user_info = json.loads(fernet.decrypt(cookie.encode()).decode())
user = self.oauth_service.lookup_user(user_info["name"])
# ...g.current_user = user

# core.sr.ht/srht/app/flask.py:267-285  (set after login)
# repo::core.sr.ht/srht/app/flask.py::make_response         (set after login)
response.set_cookie("sr.ht.unified-login.v1",
    fernet.encrypt(user_info.encode()).decode(),
    domain=global_domain, httponly=True, max_age=...)
```

The Fernet key is the shared **`[sr.ht] network-key`** from config (`core.sr.ht/srht/crypto.py:21`). meta.sr.ht sets this cookie when the user logs in; **every service — in any language — can read it, decrypt it with the same `network-key`, and know who the user is.** No per-service OAuth callback is needed just to render pages as the logged-in user.
The Fernet key is the shared **`[sr.ht] network-key`** from config (`repo::core.sr.ht/srht/crypto.py::fernet`). meta.sr.ht sets this cookie when the user logs in; **every service — in any language — can read it, decrypt it with the same `network-key`, and know who the user is.** No per-service OAuth callback is needed just to render pages as the logged-in user.

Login/logout are plain redirects to meta (`core.sr.ht/srht/app/flask.py:290-302`):
Login/logout are plain redirects to meta (`repo::core.sr.ht/srht/app/flask.py::login_url`, `::logout_url`):
```
{meta-origin}/login?return_to={your_url}
{meta-origin}/logout?return_to={your_url}
```

> Fernet = AES-128-CBC + HMAC-SHA256, base64url, with a version byte + timestamp + IV (the `cryptography` library's spec). Reimplementable in any language; Go has `github.com/fernet/fernet-go`.
> Fernet = AES-128-CBC + HMAC-SHA256, base64url, with a version byte + timestamp + IV (the `cryptography` library's spec). Reimplementable in any language; Go has `github.com/fernet/fernet-go` (the same library `core-go` uses).

### 3. The shared theme
Just a compiled CSS asset. `core.sr.ht/scss/` is the Bootstrap-derived theme; each service's `scss/main.scss` imports it and the Makefile compiles to a hashed `main.min.<sha>.css`. To match the look, **link/serve that compiled CSS** — you do not reimplement SCSS.
Just a compiled CSS asset. `repo::core.sr.ht/scss/` is the Bootstrap-derived theme; each service's `scss/main.scss` imports it and the Makefile compiles to a hashed `main.min.<sha>.css`. To match the look, **link/serve that compiled CSS** — you do not reimplement SCSS.

### 4. The GraphQL federation gateway (`api.sr.ht`)
The aggregator merges per-service GraphQL schemas into one endpoint, also config-driven:

```go
// api.sr.ht/main.go:30-35
// repo::api.sr.ht/main.go::main
for name := range conf {
    if strings.HasSuffix(name, ".sr.ht") {
        services = append(services, thistle.NewService(name, getOrigin(conf, name)))


@@ 73,10 79,10 @@ for name := range conf {
}
```

It fetches each service's `/query` schema and runs `thistle.BuildSchema(...)` (`api.sr.ht/main.go:140-168`). Add a config section pointing at your service's GraphQL `/query` endpoint and your types join the unified API — **no edit to api.sr.ht**, just config + a SIGHUP to reload (`main.go:74-87`). Internal service-to-service calls use HMAC auth (`core-go/auth/internal.go`, `api.sr.ht/auth.go` `InternalAuthTransport`).
It fetches each service's `/query` schema and runs `thistle.BuildSchema(...)` (`repo::api.sr.ht/main.go::updateSchema`). Add a config section pointing at your service's GraphQL `/query` endpoint and your types join the unified API — **no edit to api.sr.ht**, just config + a SIGHUP to reload (handled in `repo::api.sr.ht/main.go::main`). Internal service-to-service calls use HMAC auth (`repo::api.sr.ht/auth.go::InternalAuthTransport` on the caller side, `repo::core-go/auth/middleware.go::internalAuth` on the receiver side).

### 5. Webhooks
React to events from other services: GraphQL-native (`core-go/webhooks/`, `<service>/api/webhooks/`) or legacy HTTP (`core.sr.ht/srht/webhook/`). Smaller services (`paste`, `man`, `pages`) may not emit them.
React to events from other services: GraphQL-native (`repo::core-go/webhooks/queue.go::NewQueue`, plus per-service `api/webhooks/`) or legacy HTTP (`repo::core.sr.ht/srht/webhook/`). Smaller services (`paste`, `man`, `pages`) may not emit them.

---



@@ 87,13 93,13 @@ React to events from other services: GraphQL-native (`core-go/webhooks/`, `<serv
| **API / backend** | GraphQL API, DB, jobs, federation | **First-class** (`core-go` + gqlgen + thistle). The blessed path. | Yes (`core.sr.ht` GraphQL helpers) |
| **Web chrome** | nav switcher, login session, theme, rendered pages | **No shared code** — reimplement (small; see below) | **Free** via `srht.app.Flask` |

The web-chrome code (shared Jinja `layout.html`/`nav.html` + `srht.app.Flask`) lives **only in `core.sr.ht` (Python)**. `core-go` is purely API-side (`auth`, `config`, `database`, `redis`, `server`, `webhooks`, `crypto`, `objects`) — no HTML templating, no session management. The Go-only services (`api.sr.ht`, `pages.sr.ht`, `sourcehut-ssh`) render **no** integrated web UI; `pages.sr.ht` has zero `.html` files.
The web-chrome code (shared Jinja `layout.html`/`nav.html` + `repo::core.sr.ht/srht/app/flask.py::Flask`) lives **only in `core.sr.ht` (Python)**. `core-go` is purely API-side (`auth`, `config`, `database`, `redis`, `server`, `webhooks`, `crypto`, `objects`) — no HTML templating. (It does have API-side cookie auth in `repo::core-go/auth/middleware.go::cookieAuth`, but no rendered web UI / Jinja chrome.) The Go-only services (`api.sr.ht`, `pages.sr.ht`, `sourcehut-ssh`) render **no** integrated web UI; `pages.sr.ht` has zero `.html` files.

---

## Recipe A — Python web tier (the standard pattern, chrome for free)

This is how every user-facing service is built. Subclass `srht.app.Flask` and you inherit nav + unified login + theme + GraphQL blueprint + error pages. paste's entire bootstrap is ~35 lines (`paste.sr.ht/pastesrht/app.py`):
This is how every user-facing service is built. Subclass `srht.app.Flask` and you inherit nav + unified login + theme + GraphQL blueprint + error pages. paste's entire bootstrap is ~35 lines (`repo::paste.sr.ht/pastesrht/app.py::PasteApp`):

```python
from srht.app import Flask


@@ 117,12 123,12 @@ Templates `{% extends "layout.html" %}`. Build against an installed `core.sr.ht`

## Recipe B — Pure Go (the user's preference)

**API side:** straightforward and blessed — `core-go` gives you `auth` (validate meta OAuth bearer tokens: `core-go/auth/bearer.go`, `middleware.go`), `config`, `database`, `redis`, gqlgen scaffolding (`core-go/server/`), `webhooks`, `objects` (S3). Federate into `api.sr.ht` via thistle (config only).
**API side:** straightforward and blessed — `core-go` gives you `auth` (validate meta OAuth bearer tokens: `repo::core-go/auth/bearer.go::DecodeBearerToken`, `repo::core-go/auth/middleware.go::Middleware`), `config`, `database`, `redis`, gqlgen scaffolding (`repo::core-go/server/server.go::WithDefaultMiddleware`), `webhooks`, `objects` (S3). Federate into `api.sr.ht` via thistle (config only).

**Web side — reimplement the chrome, which is small:**
1. **Nav**: ~30 lines of `html/template` mirroring `nav.html`, reading the same config (`network` = config sections ending `.sr.ht`).
2. **Theme**: link/serve the already-compiled `main.min.css` from `core.sr.ht`'s build output. No reimplementation.
3. **Login/identity**: read the `sr.ht.unified-login.v1` cookie, Fernet-decrypt with `[sr.ht] network-key` (e.g. `github.com/fernet/fernet-go`), get `current_user`. For login, redirect to `{meta-origin}/login?return_to=...`; meta sets the shared cookie on the parent domain. For **write/API** actions on behalf of the user, also obtain an OAuth token (register an OAuthClient on meta, validate via `core-go/auth`).
3. **Login/identity**: read the `sr.ht.unified-login.v1` cookie, Fernet-decrypt with `[sr.ht] network-key` (`github.com/fernet/fernet-go`), get the user. For login, redirect to `{meta-origin}/login?return_to=...`; meta sets the shared cookie on the parent domain. For **write/API** actions on behalf of the user, also obtain an OAuth token (register an OAuthClient on meta, validate via `repo::core-go/auth/middleware.go::Middleware`).

That's the whole gap between Go and "looks like paste": a small nav template + a cookie-reading login handler. Everything else is `core-go`.



@@ 153,7 159,7 @@ There is no language lock-in — `core.sr.ht`/`core-go` are conveniences, not re
   connection-string=postgresql://.../myservice
   ```
   Your service also needs `[sr.ht] network-key` (the shared Fernet key) and `[meta.sr.ht] origin=` to read the login cookie and link login/register.
3. **nginx** — add a `server`/subdomain route to reach your app (`sr.ht-nginx/` style).
3. **nginx** — add a `server`/subdomain route to reach your app (`repo::sr.ht-nginx/` style).
4. **api.sr.ht** (optional federation) — the matching config section + SIGHUP; types merge in.
5. **DNS** — `myservice.srht.bigb.es` under the same global domain as the rest, so the shared cookie (`domain=.srht.bigb.es`) is visible to your service.



@@ 161,34 167,34 @@ There is no language lock-in — `core.sr.ht`/`core-go` are conveniences, not re

## Configuration field reference

SourceHut config is INI (`config.ini`, parsed by `core-go/config/config.go` and `core.sr.ht/srht/config.py`). A custom service draws from four kinds of section. **Generate keys with `sr.ht-keygen {service,network,webhook}`** — never hand-roll them.
SourceHut config is INI (`config.ini`, parsed by `repo::core-go/config/config.go::LoadConfig` and `repo::core.sr.ht/srht/config.py::load_config`). A custom service draws from four kinds of section. **Generate keys with `sr.ht-keygen {service,network,webhook}`** — never hand-roll them.

### `[sr.ht]` — global, must match the rest of the instance
| Field | Required | Who reads it | Notes |
| --- | --- | --- | --- |
| `network-key` | **Yes** | `core-go/crypto/crypto.go:38`, `core.sr.ht/srht/crypto.py:21` | **The single most important field.** Shared Fernet key used to decrypt the `sr.ht.unified-login.v1` cookie and sign internal messages. **Must be byte-identical across every service** or you can't read who's logged in. `sr.ht-keygen network`. |
| `network-key` | **Yes** | `repo::core-go/crypto/crypto.go::InitCrypto`, `repo::core.sr.ht/srht/crypto.py::fernet` | **The single most important field.** Shared Fernet key used to decrypt the `sr.ht.unified-login.v1` cookie and sign internal messages. **Must be byte-identical across every service** or you can't read who's logged in. `sr.ht-keygen network`. |
| `service-key` | Recommended | `core.sr.ht` session layer | Encrypts this service's own session cookies. May differ per service; identical is fine if you share one config. `sr.ht-keygen service`. |
| `redis-host` | If you use Redis | `core-go/server/server.go:302` | Cache / pubsub / webhook queue. Shared between nodes of a service. |
| `internal-ipnet` | Recommended | `core-go/config/config.go:71` | CIDRs trusted as internal (service-to-service) callers. Defaults to loopback+private ranges; set explicitly to match your cluster. |
| `owner-name`, `owner-email` | **Yes (Go)** | `core-go/config/config.go:89` `GetOwner` — **panics if missing** | Required by any `core-go` service at startup. |
| `redis-host` | If you use Redis | `repo::core-go/server/server.go::WithDefaultMiddleware` | Cache / pubsub / webhook queue. Shared between nodes of a service. |
| `internal-ipnet` | Recommended | `repo::core-go/config/config.go::LoadConfig` | CIDRs trusted as internal (service-to-service) callers. Defaults to loopback+private ranges; set explicitly to match your cluster. |
| `owner-name`, `owner-email` | **Yes (Go)** | `repo::core-go/config/config.go::GetOwner` — **panics if missing** | Required by any `core-go` service at startup. |
| `site-name` | Recommended | nav/templates (`site_name`) | Brand text shown in the nav across all services. |
| `environment` | Recommended | `layout.html:25` | Anything other than `production` shows a colored banner; admins always see it. |
| `environment` | Recommended | `repo::core.sr.ht/srht/templates/layout.html::ENVIRONMENT` | Anything other than `production` shows a colored banner; admins always see it. |
| `site-info`, `site-blurb`, `source-url` | Optional | templates | Cosmetic / footer links. |

### `[meta.sr.ht]` — the identity provider, always needed
| Field | Required | Notes |
| --- | --- | --- |
| `origin` | **Yes** | Used to build `login`/`logout`/register URLs (`flask.py:290-302`) and to validate tokens. Without it your service can't send users to log in. |
| `oauth-client-id`, `oauth-client-secret` | If you make API calls *as a client* | Register an OAuth client on meta and put its credentials here (pattern: `git.sr.ht/config.example.ini:169-171` declares the *integrated* service's id under its own section). Only needed for write/API actions on the user's behalf — **not** needed merely to read the login cookie. |
| `origin` | **Yes** | Used to build `login`/`logout`/register URLs (`repo::core.sr.ht/srht/app/flask.py::login_url`) and to validate tokens. Without it your service can't send users to log in. |
| `oauth-client-id`, `oauth-client-secret` | If you make API calls *as a client* | Register an OAuth client on meta and put its credentials here (pattern: `repo::git.sr.ht/config.example.ini::[builds.sr.ht]` declares the *integrated* service's id under its own section). Only needed for write/API actions on the user's behalf — **not** needed merely to read the login cookie. |

### `[myservice.sr.ht]` — your own section (the name MUST end in `.sr.ht`)
The `.sr.ht` suffix is what puts you in the nav `network` list (`flask.py:32`) and the federation loop (`api.sr.ht/main.go:31`). Pick a service "prefix" (the part before `.sr.ht`) that's unique on the instance.
The `.sr.ht` suffix is what puts you in the nav `network` list (`repo::core.sr.ht/srht/app/flask.py::_network`) and the federation loop (`repo::api.sr.ht/main.go::main`). Pick a service "prefix" (the part before `.sr.ht`) that's unique on the instance.
| Field | Required | Who reads it | Notes |
| --- | --- | --- | --- |
| `origin` | **Yes** | nav, `get_origin` (`config.py:67`, `core-go/config/config.go:107`) | External `scheme://host` your web UI is served at. Drives every cross-service link to you. |
| `connection-string` | If you have a DB | `core-go/server/server.go:288`, `core.sr.ht` `DbSession` | Postgres DSN. |
| `origin` | **Yes** | nav, `repo::core.sr.ht/srht/config.py::get_origin`, `repo::core-go/config/config.go::GetOrigin` | External `scheme://host` your web UI is served at. Drives every cross-service link to you. |
| `connection-string` | If you have a DB | `repo::core-go/server/server.go::WithDefaultMiddleware`, `core.sr.ht` `DbSession` | Postgres DSN. |
| `internal-origin` | Optional | `GetOrigin` (internal pref.) | LAN/cluster URL preferred for service-to-service traffic; falls back to `origin`. |
| `api-origin` | If you expose an API | `GetAPI` (`core-go/config/config.go:125`) | External API URL. `api.sr.ht` federates `api-origin + "/query"`. |
| `api-origin` | If you expose an API | `repo::core-go/config/config.go::GetAPI` | External API URL. `api.sr.ht` federates `api-origin + "/query"`. |
| `api-internal-origin` | Optional | `GetAPI` (internal pref.) | Internal API URL for the federation gateway / other services. |
| `migrate-on-upgrade` | Optional | packaging | `yes` to auto-run migrations on package upgrade. |
| `webhooks` | If you emit webhooks | webhook worker | Redis URL/db for the webhook queue (e.g. `redis://localhost:6379/1`). |


@@ 198,8 204,8 @@ The `.sr.ht` suffix is what puts you in the nav `network` list (`flask.py:32`) a
### `[webhooks]` — only if your service signs outgoing webhook payloads
| Field | Required | Who reads it | Notes |
| --- | --- | --- | --- |
| `private-key` | If emitting webhooks | `core-go/crypto/crypto.go:27` | base64 Ed25519 signing key, **shared across services**. `sr.ht-keygen webhook`. Distribute the public half to consumers. |
| `queue-size` | Optional | `core-go/webhooks/queue.go:47` | Defaults to `config.DefaultQueueSize`. |
| `private-key` | If emitting webhooks | `repo::core-go/crypto/crypto.go::InitCrypto` | base64 Ed25519 signing key, **shared across services**. `sr.ht-keygen webhook`. Distribute the public half to consumers. |
| `queue-size` | Optional | `repo::core-go/webhooks/queue.go::NewQueue` | Defaults to `config.DefaultQueueSize`. |

### `[mail]` — only if your service sends email
`smtp-host`, `smtp-port`, `smtp-user`, `smtp-password`, `smtp-from`, `smtp-encryption` (`starttls`/`tls`/`insecure`), `smtp-auth` (`plain`/`none`), `error-to`, `error-from`, and `pgp-privkey`/`pgp-pubkey`/`pgp-key-id` for signing. Skip the whole section if you don't email.


@@ 210,16 216,16 @@ Two reasons to mirror upstream's config vocabulary instead of designing fresh ke

**Read config through the shared helpers, not raw INI parsing** — they give you internal/external origin fallback, `api-origin` defaulting, S3 resolution, and the internal-IP trust check for free:

| Concept | Go (`core-go/config`) | Python (`srht.config`) |
| Concept | Go — `repo::core-go/config/config.go::<sym>` | Python — `repo::core.sr.ht/srht/config.py::<sym>` |
| --- | --- | --- |
| Load file | `LoadConfig()` (`config.go:31`) | `load_config()` (`config.py:23`) |
| String / int / bool | `GetString` / `GetInt` / `GetBool` (`:156`/`:167`/`:181`) | `cfg` / `cfgi` / `cfgb` (`:35`/`:44`/`:50`) |
| A service's web URL | `GetOrigin(conf, svc, external)` (`:107`) | `get_origin(svc, external=)` (`:67`) |
| A service's API URL | `GetAPI(conf, svc, external)` (`:125`) | `get_api(svc, external=)` (`:80`) |
| Owner name/email | `GetOwner` (`:89`) | — |
| Internal-caller check | `IsInternalIP(ip)` (`:198`) | — |
| Global domain | — | `get_global_domain(site)` (`:107`) |
| S3 upstream | — | `get_s3_upstream()` (`:119`) |
| Load file | `LoadConfig` | `load_config` |
| String / int / bool | `GetString` / `GetInt` / `GetBool` | `cfg` / `cfgi` / `cfgb` |
| A service's web URL | `GetOrigin` | `get_origin` |
| A service's API URL | `GetAPI` | `get_api` |
| Owner name/email | `GetOwner` | — |
| Internal-caller check | `IsInternalIP` | — |
| Global domain | — | `get_global_domain` |
| S3 upstream | — | `get_s3_upstream` |

**Use the canonical key names for your `[myservice.sr.ht]` section** — every other service uses these exact spellings, so reuse them rather than coining synonyms:



@@ 228,8 234,8 @@ Two reasons to mirror upstream's config vocabulary instead of designing fresh ke
| `origin` | `url`, `base-url`, `web-url` | `GetOrigin`/`get_origin` look up exactly `origin` (+ `internal-origin`). |
| `internal-origin` | `lan-url`, `private-origin` | The internal-preferred fallback partner of `origin`. |
| `api-origin` / `api-internal-origin` | `graphql-url`, `api-url` | `GetAPI`/`get_api` probe these names in a fixed order. |
| `connection-string` | `db`, `dsn`, `database-url`, `pg` | `core-go/server/server.go:288` and `DbSession` read `connection-string`. |
| `redis-host` | `redis`, `redis-url` | Read from `[sr.ht]`, not your section (`server.go:302`). |
| `connection-string` | `db`, `dsn`, `database-url`, `pg` | `repo::core-go/server/server.go::WithDefaultMiddleware` and `DbSession` read `connection-string`. |
| `redis-host` | `redis`, `redis-url` | Read from `[sr.ht]`, not your section (`repo::core-go/server/server.go::WithDefaultMiddleware`). |
| `migrate-on-upgrade` | `auto-migrate` | Packaging convention. |
| `webhooks` | `webhook-redis`, `hooks-db` | Redis URL for the webhook queue. |
| `debug-host` / `debug-port` | `host` / `port`, `bind` | Dev-server bind convention. |


@@ 252,11 258,10 @@ Two reasons to mirror upstream's config vocabulary instead of designing fresh ke

## Key files to re-read when working on this

- `core.sr.ht/srht/app/flask.py` — `_network` (nav list, :32), unified-login cookie read (:201-218) + write (:267-285), `login_url`/`logout_url` (:290-302)
- `core.sr.ht/srht/templates/nav.html`, `layout.html` — the shared chrome
- `core.sr.ht/srht/config.py:67` `get_origin` / `:80` `get_api`; `core.sr.ht/srht/crypto.py:21` Fernet key
- `core-go/config/config.go` — `:71` `internal-ipnet`, `:89` `GetOwner` (panics), `:107` `GetOrigin`, `:125` `GetAPI`; `core-go/crypto/crypto.go:27` webhook key / `:38` `network-key`; `core-go/server/server.go:288` `connection-string` / `:302` `redis-host`
- `*/config.example.ini` — real field names per service (`paste.sr.ht` minimal, `git.sr.ht` rich: S3, dispatch, optional `[builds.sr.ht]` integration block at `:168-171`)
- `paste.sr.ht/pastesrht/app.py` — minimal Python service bootstrap
- `api.sr.ht/main.go:26-168` — federation gateway (config-driven, thistle)
- `core-go/auth/` (`bearer.go`, `middleware.go`, `internal.go`) — Go token validation + internal HMAC
- `repo::core.sr.ht/srht/app/flask.py` — `::_network` (nav list), `::get_session_cookie` (unified-login read) + `::make_response` (write), `::login_url` / `::logout_url`, `::Flask` (the base class)
- `repo::core.sr.ht/srht/templates/nav.html`, `repo::core.sr.ht/srht/templates/layout.html` — the shared chrome
- `repo::core.sr.ht/srht/config.py::get_origin` / `::get_api`; `repo::core.sr.ht/srht/crypto.py::fernet` — Fernet key
- `repo::core-go/config/config.go` — `::LoadConfig` (incl. `internal-ipnet`), `::GetOwner` (panics), `::GetOrigin`, `::GetAPI`; `repo::core-go/crypto/crypto.go::InitCrypto` (webhook key + `network-key`); `repo::core-go/server/server.go::WithDefaultMiddleware` (`connection-string` + `redis-host`); `repo::core-go/auth/middleware.go::Middleware` / `::cookieAuth` / `::internalAuth`
- `*/config.example.ini` — real field names per service (`paste.sr.ht` minimal, `git.sr.ht` rich: S3, dispatch, optional `repo::git.sr.ht/config.example.ini::[builds.sr.ht]` integration block)
- `repo::paste.sr.ht/pastesrht/app.py::PasteApp` — minimal Python service bootstrap
- `repo::api.sr.ht/main.go::main` + `::updateSchema` — federation gateway (config-driven, thistle)