···1package db
23import (
4+ "strconv"
5 "time"
67 "tangled.sh/tangled.sh/core/api/tangled"
···100101 return keys, nil
102}
103+104+func (d *DB) GetPublicKeysPaginated(limit int, cursor string) ([]PublicKey, string, error) {
105+ var keys []PublicKey
106+107+ offset := 0
108+ if cursor != "" {
109+ if o, err := strconv.Atoi(cursor); err == nil && o >= 0 {
110+ offset = o
111+ }
112+ }
113+114+ query := `select key, did, created from public_keys order by created desc limit ? offset ?`
115+ rows, err := d.db.Query(query, limit+1, offset) // +1 to check if there are more results
116+ if err != nil {
117+ return nil, "", err
118+ }
119+ defer rows.Close()
120+121+ for rows.Next() {
122+ var publicKey PublicKey
123+ if err := rows.Scan(&publicKey.Key, &publicKey.Did, &publicKey.CreatedAt); err != nil {
124+ return nil, "", err
125+ }
126+ keys = append(keys, publicKey)
127+ }
128+129+ if err := rows.Err(); err != nil {
130+ return nil, "", err
131+ }
132+133+ // check if there are more results for pagination
134+ var nextCursor string
135+ if len(keys) > limit {
136+ keys = keys[:limit] // remove the extra item
137+ nextCursor = strconv.Itoa(offset + limit)
138+ }
139+140+ return keys, nextCursor, nil
141+}
+5
knotserver/ingester.go
···98 l := log.FromContext(ctx)
99 l = l.With("handler", "processPull")
100 l = l.With("did", did)
00000101 l = l.With("target_repo", record.Target.Repo)
102 l = l.With("target_branch", record.Target.Branch)
103
···98 l := log.FromContext(ctx)
99 l = l.With("handler", "processPull")
100 l = l.With("did", did)
101+102+ if record.Target == nil {
103+ return fmt.Errorf("ignoring pull record: target repo is nil")
104+ }
105+106 l = l.With("target_repo", record.Target.Repo)
107 l = l.With("target_branch", record.Target.Branch)
108
···4 "encoding/json"
5 "log/slog"
6 "net/http"
00708 "tangled.sh/tangled.sh/core/api/tangled"
9 "tangled.sh/tangled.sh/core/idresolver"
10 "tangled.sh/tangled.sh/core/jetstream"
···50 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
51 // - use ETags on clients to keep requests to a minimum
52 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
0000000000000000053 return r
000000000000000000000000000000000000000000000000000000000000000054}
5556func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {
···4 "encoding/json"
5 "log/slog"
6 "net/http"
7+ "net/url"
8+ "strings"
910+ securejoin "github.com/cyphar/filepath-securejoin"
11 "tangled.sh/tangled.sh/core/api/tangled"
12 "tangled.sh/tangled.sh/core/idresolver"
13 "tangled.sh/tangled.sh/core/jetstream"
···53 // - we can calculate on PR submit/resubmit/gitRefUpdate etc.
54 // - use ETags on clients to keep requests to a minimum
55 r.Post("/"+tangled.RepoMergeCheckNSID, x.MergeCheck)
56+57+ // repo query endpoints (no auth required)
58+ r.Get("/"+tangled.RepoTreeNSID, x.RepoTree)
59+ r.Get("/"+tangled.RepoLogNSID, x.RepoLog)
60+ r.Get("/"+tangled.RepoBranchesNSID, x.RepoBranches)
61+ r.Get("/"+tangled.RepoTagsNSID, x.RepoTags)
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+ // knot query endpoints (no auth required)
71+ r.Get("/"+tangled.KnotListKeysNSID, x.ListKeys)
72+73 return r
74+}
75+76+// parseRepoParam parses a repo parameter in 'did/repoName' format and returns
77+// the full repository path on disk
78+func (x *Xrpc) parseRepoParam(repo string) (string, error) {
79+ if repo == "" {
80+ return "", xrpcerr.NewXrpcError(
81+ xrpcerr.WithTag("InvalidRequest"),
82+ xrpcerr.WithMessage("missing repo parameter"),
83+ )
84+ }
85+86+ // Parse repo string (did/repoName format)
87+ parts := strings.Split(repo, "/")
88+ if len(parts) < 2 {
89+ return "", xrpcerr.NewXrpcError(
90+ xrpcerr.WithTag("InvalidRequest"),
91+ xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"),
92+ )
93+ }
94+95+ did := strings.Join(parts[:len(parts)-1], "/")
96+ repoName := parts[len(parts)-1]
97+98+ // Construct repository path using the same logic as didPath
99+ didRepoPath, err := securejoin.SecureJoin(did, repoName)
100+ if err != nil {
101+ return "", xrpcerr.NewXrpcError(
102+ xrpcerr.WithTag("RepoNotFound"),
103+ xrpcerr.WithMessage("failed to access repository"),
104+ )
105+ }
106+107+ repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath)
108+ if err != nil {
109+ return "", xrpcerr.NewXrpcError(
110+ xrpcerr.WithTag("RepoNotFound"),
111+ xrpcerr.WithMessage("failed to access repository"),
112+ )
113+ }
114+115+ return repoPath, nil
116+}
117+118+// parseStandardParams parses common query parameters used by most handlers
119+func (x *Xrpc) parseStandardParams(r *http.Request) (repo, repoPath, ref string, err error) {
120+ // Parse repo parameter
121+ repo = r.URL.Query().Get("repo")
122+ repoPath, err = x.parseRepoParam(repo)
123+ if err != nil {
124+ return "", "", "", err
125+ }
126+127+ // Parse and unescape ref parameter
128+ refParam := r.URL.Query().Get("ref")
129+ if refParam == "" {
130+ return "", "", "", xrpcerr.NewXrpcError(
131+ xrpcerr.WithTag("InvalidRequest"),
132+ xrpcerr.WithMessage("missing ref parameter"),
133+ )
134+ }
135+136+ ref, _ = url.QueryUnescape(refParam)
137+ return repo, repoPath, ref, nil
138}
139140func writeError(w http.ResponseWriter, e xrpcerr.XrpcError, status int) {