diff --git a/knotserver/xrpc/create_repo.go b/knotserver/xrpc/create_repo.go --- a/knotserver/xrpc/create_repo.go +++ b/knotserver/xrpc/create_repo.go @@ -6,18 +6,18 @@ "errors" "fmt" "net/http" - "path/filepath" + "os" "strings" "time" - comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" - "github.com/bluesky-social/indigo/xrpc" + indigoxrpc "github.com/bluesky-social/indigo/xrpc" securejoin "github.com/cyphar/filepath-securejoin" gogit "github.com/go-git/go-git/v5" "tangled.org/core/api/tangled" "tangled.org/core/hook" "tangled.org/core/knotserver/git" + "tangled.org/core/knotserver/repodid" "tangled.org/core/rbac" xrpcerr "tangled.org/core/xrpc/errors" ) @@ -51,44 +51,119 @@ return } - rkey := data.Rkey + repoName := data.Name - ident, err := h.Resolver.ResolveIdent(r.Context(), actorDid.String()) - if err != nil || ident.Handle.IsInvalidHandle() { - fail(xrpcerr.GenericError(err)) + if repoName == "" { + fail(xrpcerr.GenericError(fmt.Errorf("repository name is required"))) return } - - xrpcc := xrpc.Client{ - Host: ident.PDSEndpoint(), - } - - resp, err := comatproto.RepoGetRecord(r.Context(), &xrpcc, "", tangled.RepoNSID, actorDid.String(), rkey) - if err != nil { - fail(xrpcerr.GenericError(err)) - return - } - - repo := resp.Value.Val.(*tangled.Repo) defaultBranch := h.Config.Repo.MainBranch if data.DefaultBranch != nil && *data.DefaultBranch != "" { defaultBranch = *data.DefaultBranch } - if err := validateRepoName(repo.Name); err != nil { + if err := validateRepoName(repoName); err != nil { l.Error("creating repo", "error", err.Error()) fail(xrpcerr.GenericError(err)) return } - relativeRepoPath := filepath.Join(actorDid.String(), repo.Name) - repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, relativeRepoPath) + var repoDid string + var prepared *repodid.PreparedDID + + knotServiceUrl := "https://" + h.Config.Server.Hostname + if h.Config.Server.Dev { + knotServiceUrl = "http://" + h.Config.Server.Hostname + } + + switch { + case data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:"): + if err := repodid.VerifyRepoDIDWeb(r.Context(), h.Resolver, *data.RepoDid, knotServiceUrl); err != nil { + l.Error("verifying did:web", "error", err.Error()) + writeError(w, xrpcerr.GenericError(err), http.StatusBadRequest) + return + } + + exists, err := h.Db.RepoDidExists(*data.RepoDid) + if err != nil { + l.Error("checking did:web uniqueness", "error", err.Error()) + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + if exists { + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use on this knot", *data.RepoDid)), http.StatusConflict) + return + } + + repoDid = *data.RepoDid + + case data.RepoDid != nil && *data.RepoDid != "": + writeError(w, xrpcerr.GenericError(fmt.Errorf("only did:web is accepted as a user-provided repo DID; did:plc is auto-generated")), http.StatusBadRequest) + return + + default: + existingDid, dbErr := h.Db.GetRepoDid(actorDid.String(), repoName) + if dbErr == nil && existingDid != "" { + didRepoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, existingDid) + if _, statErr := os.Stat(didRepoPath); statErr == nil { + l.Info("repo already exists from previous attempt", "repoDid", existingDid) + output := tangled.RepoCreate_Output{RepoDid: &existingDid} + writeJson(w, &output) + return + } + l.Warn("stale repo key found without directory, cleaning up", "repoDid", existingDid) + if delErr := h.Db.DeleteRepoKey(existingDid); delErr != nil { + l.Error("failed to clean up stale repo key", "repoDid", existingDid, "error", delErr.Error()) + writeError(w, xrpcerr.GenericError(fmt.Errorf("failed to clean up stale state, retry later")), http.StatusInternalServerError) + return + } + } + + var prepErr error + prepared, prepErr = repodid.PrepareRepoDID(h.Config.Server.PlcUrl, knotServiceUrl) + if prepErr != nil { + l.Error("preparing repo DID", "error", prepErr.Error()) + writeError(w, xrpcerr.GenericError(prepErr), http.StatusInternalServerError) + return + } + repoDid = prepared.RepoDid + + atUri := fmt.Sprintf("at://%s/%s/%s", actorDid, tangled.RepoNSID, data.Rkey) + if err := h.Db.StoreRepoKey(repoDid, prepared.SigningKeyRaw, actorDid.String(), repoName, atUri); err != nil { + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + writeError(w, xrpcerr.GenericError(fmt.Errorf("repository %s already being created", repoName)), http.StatusConflict) + return + } + l.Error("claiming repo key slot", "error", err.Error()) + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + } + + l = l.With("repoDid", repoDid) + + repoPath, _ := securejoin.SecureJoin(h.Config.Repo.ScanPath, repoDid) + rbacPath := repoDid + + cleanup := func() { + if rmErr := os.RemoveAll(repoPath); rmErr != nil { + l.Error("failed to clean up repo directory", "path", repoPath, "error", rmErr.Error()) + } + } + + cleanupAll := func() { + cleanup() + if delErr := h.Db.DeleteRepoKey(repoDid); delErr != nil { + l.Error("failed to clean up repo key", "error", delErr.Error()) + } + } if data.Source != nil && *data.Source != "" { err = git.Fork(repoPath, *data.Source, h.Config) if err != nil { l.Error("forking repo", "error", err.Error()) + cleanupAll() writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) return } @@ -96,20 +171,46 @@ err = git.InitBare(repoPath, defaultBranch) if err != nil { l.Error("initializing bare repo", "error", err.Error()) + cleanupAll() if errors.Is(err, gogit.ErrRepositoryAlreadyExists) { fail(xrpcerr.RepoExistsError("repository already exists")) return - } else { - writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + } + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + } + + if data.RepoDid != nil && strings.HasPrefix(*data.RepoDid, "did:web:") { + webAtUri := fmt.Sprintf("at://%s/%s/%s", actorDid, tangled.RepoNSID, data.Rkey) + if err := h.Db.StoreRepoDidWeb(repoDid, actorDid.String(), repoName, webAtUri); err != nil { + cleanupAll() + if strings.Contains(err.Error(), "UNIQUE constraint failed") { + writeError(w, xrpcerr.GenericError(fmt.Errorf("did:web %s is already in use", repoDid)), http.StatusConflict) return } + l.Error("storing did:web repo entry", "error", err.Error()) + writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) + return + } + } + + if prepared != nil { + plcCtx, plcCancel := context.WithTimeout(context.Background(), 30*time.Second) + defer plcCancel() + if err := prepared.Submit(plcCtx); err != nil { + l.Error("submitting to PLC directory", "error", err.Error()) + cleanupAll() + writeError(w, xrpcerr.GenericError(fmt.Errorf("PLC directory submission failed: %w", err)), http.StatusInternalServerError) + return } } // add perms for this user to access the repo - err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, relativeRepoPath) + err = h.Enforcer.AddRepo(actorDid.String(), rbac.ThisServer, rbacPath) if err != nil { l.Error("adding repo permissions", "error", err.Error()) + cleanupAll() writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) return } @@ -127,7 +228,7 @@ // Therefore, to bypass the local tap, requestCrawl directly to the knotmirror. go func() { if h.Config.Server.Dev { - repoAt := fmt.Sprintf("at://%s/%s/%s", actorDid, tangled.RepoNSID, rkey) + repoAt := fmt.Sprintf("at://%s/%s/%s", actorDid, tangled.RepoNSID, data.Rkey) rCtx, rCancel := context.WithTimeout(context.Background(), 10*time.Second) defer rCancel() h.requestCrawl(rCtx, &tangled.SyncRequestCrawl_Input{ @@ -137,13 +238,13 @@ } }() - w.WriteHeader(http.StatusOK) + writeJson(w, &tangled.RepoCreate_Output{RepoDid: &repoDid}) } func (h *Xrpc) requestCrawl(ctx context.Context, input *tangled.SyncRequestCrawl_Input) error { h.Logger.Info("requesting crawl", "mirrors", h.Config.KnotMirrors) for _, knotmirror := range h.Config.KnotMirrors { - xrpcc := xrpc.Client{Host: knotmirror} + xrpcc := indigoxrpc.Client{Host: knotmirror} if err := tangled.SyncRequestCrawl(ctx, &xrpcc, input); err != nil { h.Logger.Error("error requesting crawl", "err", err) } else { diff --git a/knotserver/xrpc/delete_branch.go b/knotserver/xrpc/delete_branch.go --- a/knotserver/xrpc/delete_branch.go +++ b/knotserver/xrpc/delete_branch.go @@ -8,7 +8,6 @@ comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/rbac" @@ -57,20 +56,24 @@ } repo := resp.Value.Val.(*tangled.Repo) - didPath, err := securejoin.SecureJoin(ident.DID.String(), repo.Name) + repoDid, err := x.Db.GetRepoDid(ident.DID.String(), repo.Name) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { - l.Error("insufficent permissions", "did", actorDid.String(), "repo", didPath) + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { + l.Error("insufficent permissions", "did", actorDid.String(), "repo", repoDid) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return } - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) - gr, err := git.PlainOpen(path) + gr, err := git.PlainOpen(repoPath) if err != nil { fail(xrpcerr.GenericError(err)) return diff --git a/knotserver/xrpc/delete_repo.go b/knotserver/xrpc/delete_repo.go --- a/knotserver/xrpc/delete_repo.go +++ b/knotserver/xrpc/delete_repo.go @@ -5,12 +5,10 @@ "fmt" "net/http" "os" - "path/filepath" comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/rbac" xrpcerr "tangled.org/core/xrpc/errors" @@ -61,20 +59,24 @@ return } - relativeRepoPath := filepath.Join(did, name) - isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath) + repoDid, err := x.Db.GetRepoDid(did, name) + if err != nil { + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) + return + } + + isDeleteAllowed, err := x.Enforcer.IsRepoDeleteAllowed(actorDid.String(), rbac.ThisServer, repoDid) if err != nil { fail(xrpcerr.GenericError(err)) return } if !isDeleteAllowed { fail(xrpcerr.AccessControlError(actorDid.String())) - return - } - - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) - if err != nil { - fail(xrpcerr.GenericError(err)) return } @@ -85,11 +87,15 @@ return } - err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, relativeRepoPath) + err = x.Enforcer.RemoveRepo(did, rbac.ThisServer, repoDid) if err != nil { l.Error("failed to delete repo from enforcer", "error", err.Error()) writeError(w, xrpcerr.GenericError(err), http.StatusInternalServerError) return + } + + if err := x.Db.DeleteRepoKey(repoDid); err != nil { + l.Error("failed to delete repo key", "error", err.Error()) } w.WriteHeader(http.StatusOK) diff --git a/knotserver/xrpc/fork_status.go b/knotserver/xrpc/fork_status.go --- a/knotserver/xrpc/fork_status.go +++ b/knotserver/xrpc/fork_status.go @@ -7,7 +7,6 @@ "path/filepath" "github.com/bluesky-social/indigo/atproto/syntax" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/rbac" @@ -51,17 +50,20 @@ name = filepath.Base(source) } - relativeRepoPath := filepath.Join(did, name) - - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) + repoDid, err := x.Db.GetRepoDid(did, name) + if err != nil { + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) - if err != nil { - fail(xrpcerr.GenericError(err)) + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return } diff --git a/knotserver/xrpc/fork_sync.go b/knotserver/xrpc/fork_sync.go --- a/knotserver/xrpc/fork_sync.go +++ b/knotserver/xrpc/fork_sync.go @@ -4,10 +4,8 @@ "encoding/json" "fmt" "net/http" - "path/filepath" "github.com/bluesky-social/indigo/atproto/syntax" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/rbac" @@ -42,17 +40,20 @@ return } - relativeRepoPath := filepath.Join(did, name) - - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) - writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) + repoDid, err := x.Db.GetRepoDid(did, name) + if err != nil { + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) - if err != nil { - fail(xrpcerr.GenericError(err)) + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) + writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return } diff --git a/knotserver/xrpc/hidden_ref.go b/knotserver/xrpc/hidden_ref.go --- a/knotserver/xrpc/hidden_ref.go +++ b/knotserver/xrpc/hidden_ref.go @@ -8,7 +8,6 @@ comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/rbac" @@ -63,21 +62,20 @@ } repo := resp.Value.Val.(*tangled.Repo) - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { - l.Error("insufficient permissions", "did", actorDid.String(), "repo", didPath) + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) - return - } - - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) - if err != nil { - fail(xrpcerr.GenericError(err)) return } diff --git a/knotserver/xrpc/merge.go b/knotserver/xrpc/merge.go --- a/knotserver/xrpc/merge.go +++ b/knotserver/xrpc/merge.go @@ -7,7 +7,6 @@ "net/http" "github.com/bluesky-social/indigo/atproto/syntax" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/patchutil" @@ -43,21 +42,20 @@ return } - relativeRepoPath, err := securejoin.SecureJoin(did, name) + repoDid, err := x.Db.GetRepoDid(did, name) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, relativeRepoPath); !ok || err != nil { - l.Error("insufficient permissions", "did", actorDid.String(), "repo", relativeRepoPath) + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { + l.Error("insufficient permissions", "did", actorDid.String(), "repo", repoDid) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) - return - } - - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) - if err != nil { - fail(xrpcerr.GenericError(err)) return } diff --git a/knotserver/xrpc/merge_check.go b/knotserver/xrpc/merge_check.go --- a/knotserver/xrpc/merge_check.go +++ b/knotserver/xrpc/merge_check.go @@ -6,7 +6,6 @@ "fmt" "net/http" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/patchutil" @@ -34,15 +33,14 @@ return } - relativeRepoPath, err := securejoin.SecureJoin(did, name) + repoDid, err := x.Db.GetRepoDid(did, name) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) return } - - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, relativeRepoPath) + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) return } diff --git a/knotserver/xrpc/set_default_branch.go b/knotserver/xrpc/set_default_branch.go --- a/knotserver/xrpc/set_default_branch.go +++ b/knotserver/xrpc/set_default_branch.go @@ -8,7 +8,6 @@ comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" "github.com/bluesky-social/indigo/xrpc" - securejoin "github.com/cyphar/filepath-securejoin" "tangled.org/core/api/tangled" "tangled.org/core/knotserver/git" "tangled.org/core/rbac" @@ -59,20 +58,24 @@ } repo := resp.Value.Val.(*tangled.Repo) - didPath, err := securejoin.SecureJoin(actorDid.String(), repo.Name) + repoDid, err := x.Db.GetRepoDid(actorDid.String(), repo.Name) if err != nil { - fail(xrpcerr.GenericError(err)) + fail(xrpcerr.RepoNotFoundError) + return + } + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if err != nil { + fail(xrpcerr.RepoNotFoundError) return } - if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil { + if ok, err := x.Enforcer.IsPushAllowed(actorDid.String(), rbac.ThisServer, repoDid); !ok || err != nil { l.Error("insufficent permissions", "did", actorDid.String()) writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized) return } - path, _ := securejoin.SecureJoin(x.Config.Repo.ScanPath, didPath) - gr, err := git.PlainOpen(path) + gr, err := git.PlainOpen(repoPath) if err != nil { fail(xrpcerr.GenericError(err)) return diff --git a/knotserver/xrpc/xrpc.go b/knotserver/xrpc/xrpc.go --- a/knotserver/xrpc/xrpc.go +++ b/knotserver/xrpc/xrpc.go @@ -4,9 +4,12 @@ "encoding/json" "log/slog" "net/http" + "os" + "path/filepath" "strings" securejoin "github.com/cyphar/filepath-securejoin" + "github.com/go-chi/chi/v5" "tangled.org/core/api/tangled" "tangled.org/core/idresolver" "tangled.org/core/jetstream" @@ -16,8 +19,6 @@ "tangled.org/core/rbac" xrpcerr "tangled.org/core/xrpc/errors" "tangled.org/core/xrpc/serviceauth" - - "github.com/go-chi/chi/v5" ) type Xrpc struct { @@ -78,39 +79,40 @@ return r } -// parseRepoParam parses a repo parameter in 'did/repoName' format and returns -// the full repository path on disk func (x *Xrpc) parseRepoParam(repo string) (string, error) { - if repo == "" { + if repo == "" || !strings.HasPrefix(repo, "did:") { return "", xrpcerr.NewXrpcError( xrpcerr.WithTag("InvalidRequest"), - xrpcerr.WithMessage("missing repo parameter"), + xrpcerr.WithMessage("missing or invalid repo parameter, expected a repo DID"), ) } - // Parse repo string (did/repoName format) + if !strings.Contains(repo, "/") { + repoPath, _, _, err := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repo) + if err != nil { + return "", xrpcerr.RepoNotFoundError + } + return repoPath, nil + } + parts := strings.SplitN(repo, "/", 2) - if len(parts) != 2 { - return "", xrpcerr.NewXrpcError( - xrpcerr.WithTag("InvalidRequest"), - xrpcerr.WithMessage("invalid repo format, expected 'did/repoName'"), - ) + ownerDid, repoName := parts[0], parts[1] + + repoDid, err := x.Db.GetRepoDid(ownerDid, repoName) + if err == nil { + repoPath, _, _, resolveErr := x.Db.ResolveRepoDIDOnDisk(x.Config.Repo.ScanPath, repoDid) + if resolveErr == nil { + return repoPath, nil + } } - did := parts[0] - repoName := parts[1] - - // Construct repository path using the same logic as didPath - didRepoPath, err := securejoin.SecureJoin(did, repoName) - if err != nil { + repoPath, joinErr := securejoin.SecureJoin(x.Config.Repo.ScanPath, filepath.Join(ownerDid, repoName)) + if joinErr != nil { return "", xrpcerr.RepoNotFoundError } - - repoPath, err := securejoin.SecureJoin(x.Config.Repo.ScanPath, didRepoPath) - if err != nil { + if _, statErr := os.Stat(repoPath); statErr != nil { return "", xrpcerr.RepoNotFoundError } - return repoPath, nil }