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