Monorepo for Tangled
1package xrpc
2
3import (
4 "encoding/json"
5 "log/slog"
6 "net/http"
7 "strings"
8
9 "tangled.org/core/api/tangled"
10 "tangled.org/core/idresolver"
11 "tangled.org/core/jetstream"
12 "tangled.org/core/knotserver/config"
13 "tangled.org/core/knotserver/db"
14 "tangled.org/core/notifier"
15 "tangled.org/core/rbac"
16 xrpcerr "tangled.org/core/xrpc/errors"
17 "tangled.org/core/xrpc/serviceauth"
18
19 "github.com/go-chi/chi/v5"
20)
21
22type Xrpc struct {
23 Config *config.Config
24 Db *db.DB
25 Ingester *jetstream.JetstreamClient
26 Enforcer *rbac.Enforcer
27 Logger *slog.Logger
28 Notifier *notifier.Notifier
29 Resolver *idresolver.Resolver
30 ServiceAuth *serviceauth.ServiceAuth
31}
32
33func (x *Xrpc) Router() http.Handler {
34 r := chi.NewRouter()
35
36 r.Group(func(r chi.Router) {
37 r.Use(x.ServiceAuth.VerifyServiceAuth)
38
39 r.Post("/"+tangled.RepoSetDefaultBranchNSID, x.SetDefaultBranch)
40 r.Post("/"+tangled.RepoDeleteBranchNSID, x.DeleteBranch)
41 r.Post("/"+tangled.RepoCreateNSID, x.CreateRepo)
42 r.Post("/"+tangled.RepoDeleteNSID, x.DeleteRepo)
43 r.Post("/"+tangled.RepoForkStatusNSID, x.ForkStatus)
44 r.Post("/"+tangled.RepoForkSyncNSID, x.ForkSync)
45 r.Post("/"+tangled.RepoHiddenRefNSID, x.HiddenRef)
46 r.Post("/"+tangled.RepoMergeNSID, x.Merge)
47 })
48
49 // merge check is an open endpoint
50 //
51 // TODO: should we constrain this more?
52 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
53 // - use ETags on clients to keep requests to a minimum
54 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
55
56 // repo query endpoints (no auth required)
57 r.Get("/"+tangled.RepoTreeNSID, x.RepoTree)
58 r.Get("/"+tangled.RepoLogNSID, x.RepoLog)
59 r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches)
60 r.Get("/"+tangled.RepoTagsNSID, x.RepoTags)
61 r.Get("/"+tangled.RepoTagNSID, x.RepoTag)
62 r.Get("/"+tangled.RepoBlobNSID, x.RepoBlob)
63 r.Get("/"+tangled.RepoDiffNSID, x.RepoDiff)
64 r.Get("/"+tangled.RepoCompareNSID, x.RepoCompare)
65 r.Get("/"+tangled.RepoGetDefaultBranchNSID, x.RepoGetDefaultBranch)
66 r.Get("/"+tangled.RepoBranchNSID, x.RepoBranch)
67 r.Get("/"+tangled.RepoArchiveNSID, x.RepoArchive)
68 r.Get("/"+tangled.RepoLanguagesNSID, x.RepoLanguages)
69
70 r.Get("/"+tangled.RepoResolveAtUriNSID, x.ResolveAtUri)
71
72 // knot query endpoints (no auth required)
73 r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
74 r.Get("/"+tangled.KnotVersionNSID, x.Version)
75
76 // service query endpoints (no auth required)
77 r.Get("/"+tangled.OwnerNSID, x.Owner)
78
79 return r
80}
81
82// parseRepoParam parses a repo parameter in 'did/repoName' or bare 'did:...' format
83// and returns the full repository path on disk
84func (x *Xrpc) parseRepoParam(repo string) (string, error) {
85 if repo == "" {
86 return "", xrpcerr.NewXrpcError(
87 xrpcerr.WithTag("InvalidRequest"),
88 xrpcerr.WithMessage("missing repo parameter"),
89 )
90 }
91
92 if strings.HasPrefix(repo, "did:") && !strings.Contains(repo, "/") {
93 repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo)
94 if err != nil {
95 return "", xrpcerr.RepoNotFoundError
96 }
97 return repoPath, nil
98 }
99
100 parts := strings.SplitN(repo, "/", 2)
101 if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
102 return "", xrpcerr.NewXrpcError(
103 xrpcerr.WithTag("InvalidRequest"),
104 xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
105 )
106 }
107
108 repoPath, _, err := x.Db.ResolveRepoOnDisk(x.Config.Repo.ScanPath, parts[0], parts[1])
109 if err != nil {
110 return "", xrpcerr.RepoNotFoundError
111 }
112 return repoPath, nil
113}
114
115func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
116 w.Header().Set("Content-Type", "application/json")
117 w.WriteHeader(status)
118 json.NewEncoder(w).Encode(e)
119}
120
121func writeJson(w http.ResponseWriter, response any) {
122 w.Header().Set("Content-Type", "application/json")
123 if err := json.NewEncoder(w).Encode(response); err != nil {
124 writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError)
125 return
126 }
127}