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