Federated via graft
0

Configure Feed

Select the types of activity you want to include in your feed.

v2 M4: expose each series' Radicle DID on its ActivityPub profile

RepoSyncer.RadicleDID() resolves the local node identity the same way
GitSyncer already does for remote setup (rad self --nid), and the actor
object now carries it as a PropertyValue attachment — so anyone following
from the fediverse can see, right on the profile, which Radicle node
actually signs this repo's history.

Deliberately scoped down from "AT Proto DID bridging" as discussed: this
surfaces the DID as verifiable profile metadata, not a cross-protocol
identity resolution (that would need a real DID method resolver and a
PDS to test against, neither of which exist here).

Verified live: https://graft.cyberwild.org/actors/federation-x now
carries the correct did:key: attachment.

+55 -8
+16
cmd/sync/main.go
··· 48 48 49 49 syncers := make([]*sync.RepoSyncer, 0, len(cfg.Repos)) 50 50 syncersByPair := map[string]*sync.RepoSyncer{} 51 + syncersBySeries := map[string]*sync.RepoSyncer{} 51 52 for _, pair := range cfg.Repos { 52 53 rs, err := sync.New(pair, st, sync.WorkDirFor(stateDir, pair.Name)) 53 54 if err != nil { ··· 56 57 } 57 58 syncers = append(syncers, rs) 58 59 syncersByPair[pair.Name] = rs 60 + if _, ok := syncersBySeries[rs.Series()]; !ok { 61 + syncersBySeries[rs.Series()] = rs 62 + } 59 63 } 60 64 61 65 topology := buildTopology(cfg) ··· 83 87 return fmt.Errorf("unknown repo pair %q", repoPair) 84 88 } 85 89 return rs.CommentOnItem(kind, forgejoID, radicleID, body) 90 + }, 91 + func(series string) string { 92 + rs, ok := syncersBySeries[series] 93 + if !ok { 94 + return "" 95 + } 96 + did, err := rs.RadicleDID() 97 + if err != nil { 98 + log.Error("resolve radicle did", "series", series, "err", err) 99 + return "" 100 + } 101 + return did 86 102 }, 87 103 ) 88 104 }
+12 -3
internal/activitypub/actor.go
··· 39 39 40 40 // BuildActor constructs the actor object for a series. repoURL links to 41 41 // the underlying repo (its Forgejo or Radicle explorer page); publicKeyPEM 42 - // is that series' stored public key. 43 - func BuildActor(host, series, repoURL, publicKeyPEM string) Actor { 42 + // is that series' stored public key; radicleDID is that series' Radicle 43 + // node identity (did:key:..., empty to omit), shown as a verifiable 44 + // profile field so anyone following from the fediverse can confirm which 45 + // Radicle node actually signs this repo's history. 46 + func BuildActor(host, series, repoURL, publicKeyPEM, radicleDID string) Actor { 44 47 base := ActorURI(host, series) 45 - return Actor{ 48 + a := Actor{ 46 49 Context: []string{"https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1"}, 47 50 ID: base, 48 51 Type: "Service", ··· 59 62 PublicKeyPem: publicKeyPEM, 60 63 }, 61 64 } 65 + if radicleDID != "" { 66 + a.Attachment = []Attachment{ 67 + {Type: "PropertyValue", Name: "Radicle DID", Value: radicleDID}, 68 + } 69 + } 70 + return a 62 71 }
+9 -5
internal/activitypub/handlers.go
··· 28 28 store *state.Store 29 29 host string 30 30 log *slog.Logger 31 - // repoURL, known, and postComment are supplied by the caller 32 - // (cmd/sync/main.go), which already knows every series' topology and 33 - // holds the actual sync.RepoSyncer instances — avoids this package 34 - // needing to know about config.Config or sync.RepoSyncer at all. 31 + // repoURL, known, postComment, and radicleDID are supplied by the 32 + // caller (cmd/sync/main.go), which already knows every series' 33 + // topology and holds the actual sync.RepoSyncer instances — avoids 34 + // this package needing to know about config.Config or sync.RepoSyncer 35 + // at all. 35 36 repoURL func(series string) string 36 37 known func(series string) bool 37 38 postComment func(repoPair, kind string, forgejoID int64, radicleID, body string) error 39 + radicleDID func(series string) string 38 40 client *http.Client 39 41 } 40 42 ··· 45 47 repoURL func(series string) string, 46 48 known func(series string) bool, 47 49 postComment func(repoPair, kind string, forgejoID int64, radicleID, body string) error, 50 + radicleDID func(series string) string, 48 51 ) *Handler { 49 52 return &Handler{ 50 53 store: store, ··· 53 56 repoURL: repoURL, 54 57 known: known, 55 58 postComment: postComment, 59 + radicleDID: radicleDID, 56 60 client: &http.Client{Timeout: 15 * time.Second}, 57 61 } 58 62 } ··· 152 156 http.Error(w, err.Error(), http.StatusInternalServerError) 153 157 return 154 158 } 155 - writeJSON(w, activityContentType, BuildActor(h.host, series, h.repoURL(series), pub)) 159 + writeJSON(w, activityContentType, BuildActor(h.host, series, h.repoURL(series), pub, h.radicleDID(series))) 156 160 } 157 161 158 162 func (h *Handler) outbox(w http.ResponseWriter, series string) {
+18
internal/sync/syncer.go
··· 152 152 return errors.Join(errs...) 153 153 } 154 154 155 + // RadicleDID returns the local Radicle node identity this pair pushes as, 156 + // resolved the same way GitSyncer resolves it for remote setup (`rad self 157 + // --nid`). Empty if this pair has no git sync configured, so nothing to 158 + // report. 159 + func (rs *RepoSyncer) RadicleDID() (string, error) { 160 + if rs.git == nil { 161 + return "", nil 162 + } 163 + nid, err := rs.git.localNodeID() 164 + if err != nil { 165 + return "", err 166 + } 167 + if nid == "" { 168 + return "", nil 169 + } 170 + return "did:key:" + nid, nil 171 + } 172 + 155 173 // Run executes every enabled scope once, logging and continuing past 156 174 // per-scope errors so one broken scope doesn't block the others. It 157 175 // returns each scope's error (nil if disabled or successful) for the