Two pictures of how this goes — what I'm building now, and where I'd like to push it.
0
forego-knot-gist.md edited
63 lines 3.2 kB view raw view rendered
1## Today — Forgejo private, public repos mirrored to `knot1.tangled.sh` 2 3Forgejo holds canonical state. A small bridge translates Forgejo webhook events into AT Proto XRPC calls and signed git pushes against the hosted knot. Forgejo image untouched. 4 5```mermaid 6flowchart LR 7 user(["author"]) -->|git push| forgejo 8 9 subgraph private["my forge — private"] 10 forgejo["Forgejo<br/>(authoritative)"] 11 webhook[/"webhook config<br/>per repo or org"/] 12 forgejo --- webhook 13 end 14 15 webhook -->|push / create / delete<br/>HMAC-signed| bridge 16 17 subgraph bridgeBox["forgejo-knot-bridge — small Go service"] 18 bridge["event handler<br/>user → DID mapping"] 19 keystore[("signing keys<br/>per bridged DID")] 20 statedb[("bridge.db<br/>repo→DID,<br/>last-mirrored ref")] 21 bridge --- keystore 22 bridge --- statedb 23 end 24 25 bridge -->|sh.tangled.repo.create<br/>Service-Auth JWT| knot 26 bridge -->|git push --mirror<br/>Service-Auth Bearer| knot 27 28 subgraph hosted["knot1.tangled.sh — hosted, third-party"] 29 knot["knotserver"] 30 appview["tangled.sh appview<br/>(consumes firehose)"] 31 knot -.->|sh.tangled.knot.subscribeRepos<br/>ws firehose| appview 32 end 33 34 plc[("plc.directory")] -.->|DID document<br/>verifies signing key| knot 35``` 36 37The bridge needs three things from the knot side: knot URL + DID, `server:member` role for the DIDs being published as, and signing keys for those DIDs. From Forgejo it needs a webhook subscription, public-repo read access, and a Forgejo-user → DID mapping. 38 39--- 40 41## Tomorrow — Forgejo *is* the knot 42 43Instead of bridging Forgejo *into* a separate knotserver, teach Forgejo to *be* a knot. Same Forgejo binary, same `codeberg.org` hostname, with a module that exposes the knot lexicon surface backed by Forgejo's existing repo storage. 44 45```mermaid 46flowchart LR 47 subgraph today["Today (above)"] 48 f1["Forgejo"] -->|webhook| b1["bridge<br/>(separate service)"] 49 b1 -->|XRPC + git push| k1["knotserver<br/>(separate process)"] 50 end 51 52 subgraph proposed["Forgejo as a knot"] 53 forgejo["Forgejo<br/>+ knot-frontend module<br/>(or external sidecar)"] 54 users(["users<br/>(unchanged web/SSH/REST)"]) --> forgejo 55 forgejo -.->|"/xrpc/sh.tangled.*<br/>/events firehose<br/>Service-Auth on writes"| world["AT Protocol clients<br/>(tangled.sh appview,<br/>knotmirror, ...)"] 56 end 57 58 today -.->|same protocol surface,<br/>different deployment| proposed 59``` 60 61The mapping is mostly mechanical — `sh.tangled.repo.{create,delete,tree,log,branches,diff,...}` lands on Forgejo's existing repo APIs in XRPC envelopes; `sh.tangled.knot.listKeys` reuses Forgejo's user SSH keys; identity reconciles via a `did` field on Forgejo's user table. 62 63No-trivial part is `sh.tangled.knot.subscribeRepos` — an atproto-style sequenced WebSocket subscription with `cursor` resume and `ConsumerTooSlow` semantics. Forgejo's existing internal event hooks (the same ones driving its ActivityPub outbound delivery) are the natural source; you translate ref-update events into the lexicon's wire format and replay from a `tangled_firehose_events` table on reconnect.