From d78be590071fbdbea4719a940a9cdfcf37fef35c Mon Sep 17 00:00:00 2001 From: Lewis Date: Thu, 30 Jul 2026 08:59:23 +0000 Subject: [PATCH] repoverify,idresolver/mock: extract Describe, resolve knots via KnotURL Lewis: May this revision serve well! --- appview/ingester_repo.go | 4 ++-- appview/ingester_repo_test.go | 7 +++---- idresolver/mock.go | 27 ++++++++++++++++++++++++++- repoverify/verify.go | 139 +++++++++++++++++++++++++++++++++++++++---------------------------------------------------------------------------------------------------- repoverify/verify_test.go | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------- spindle/server.go | 2 +- spindle/tapclient_test.go | 22 +--------------------- knotmirror/xrpc/proxy.go | 65 +++++++++++++++++------------------------------------------------ knotserver/keys/keys_test.go | 33 ++++++++------------------------- knotserver/repodid/repodid.go | 20 ++++++++++++++------ spindle/xrpc/xrpc_test.go | 22 +--------------------- 11 file(s) changed, 196 insertion(s)(+), 263 deletion(s)(-) diff --git a/appview/ingester_repo.go b/appview/ingester_repo.go --- a/appview/ingester_repo.go +++ b/appview/ingester_repo.go @@ -443,11 +443,11 @@ ) return false, nil } - if !strings.EqualFold(recordKnot, result.KnotURL.Host) { + if !strings.EqualFold(recordKnot, result.KnotURL.Host()) { l.Warn("rejecting repo event: record knot does not match DID-doc endpoint", "repoDid", repoDid, "recordKnot", recordKnot, - "canonicalKnot", result.KnotURL.Host, + "canonicalKnot", result.KnotURL.Host(), ) return false, nil } diff --git a/appview/ingester_repo_test.go b/appview/ingester_repo_test.go --- a/appview/ingester_repo_test.go +++ b/appview/ingester_repo_test.go @@ -6,7 +6,6 @@ "encoding/json" "errors" "log/slog" - "net/url" "path/filepath" "testing" @@ -22,11 +21,11 @@ "tangled.org/core/repoverify" ) -func mustKnotURL(t *testing.T, raw string) *url.URL { +func mustKnotURL(t *testing.T, raw string) repoident.KnotURL { t.Helper() - u, err := repoverify.ParseKnotEndpoint(raw, true) + u, err := repoident.ParseKnotURL(raw, repoident.AllowHTTP) if err != nil { - t.Fatalf("ParseKnotEndpoint(%q): %v", raw, err) + t.Fatalf("ParseKnotURL(%q): %v", raw, err) } return u } diff --git a/idresolver/mock.go b/idresolver/mock.go --- a/idresolver/mock.go +++ b/idresolver/mock.go @@ -1,10 +1,35 @@ package idresolver -import "github.com/bluesky-social/indigo/atproto/identity" +import ( + "context" + + "github.com/bluesky-social/indigo/atproto/identity" + "github.com/bluesky-social/indigo/atproto/syntax" +) func NewMockResolver(dir identity.Directory) *Resolver { return &Resolver{ directory: dir, base: &identity.BaseDirectory{}, } +} + +type MockDirectory struct { + Ident *identity.Identity +} + +func (m MockDirectory) LookupDID(context.Context, syntax.DID) (*identity.Identity, error) { + return m.Ident, nil +} + +func (m MockDirectory) LookupHandle(context.Context, syntax.Handle) (*identity.Identity, error) { + return m.Ident, nil +} + +func (m MockDirectory) Lookup(context.Context, syntax.AtIdentifier) (*identity.Identity, error) { + return m.Ident, nil +} + +func (m MockDirectory) Purge(context.Context, syntax.AtIdentifier) error { + return nil } diff --git a/repoverify/verify.go b/repoverify/verify.go --- a/repoverify/verify.go +++ b/repoverify/verify.go @@ -4,49 +4,47 @@ "context" "errors" "fmt" - "net" "net/http" - "net/url" - "syscall" "time" + "github.com/bluesky-social/indigo/atproto/syntax" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "tangled.org/core/api/tangled" + "tangled.org/core/hostutil" "tangled.org/core/idresolver" "tangled.org/core/repoident" "tangled.org/core/xrpc/xrpcclient" ) -func ParseKnotEndpoint(raw string, dev bool) (*url.URL, error) { - if raw == "" { - return nil, fmt.Errorf("empty knot URL") - } - u, err := url.Parse(raw) - if err != nil { - return nil, fmt.Errorf("invalid knot URL %q: %w", raw, err) - } - if u.Host == "" { - return nil, fmt.Errorf("knot URL %q has no host", raw) - } - switch u.Scheme { - case "https": - case "http": - if !dev { - return nil, fmt.Errorf("knot URL %q must use https outside dev mode", raw) - } - default: - return nil, fmt.Errorf("knot URL %q has unsupported scheme %q", raw, u.Scheme) - } - return u, nil -} - type Result struct { RepoDid repoident.RepoDid OwnerDid repoident.OwnerDid - KnotURL *url.URL + KnotURL repoident.KnotURL // Rkey of the sh.tangled.repo record tracked by the knot; empty when the // knot does not support describeRepo. - Rkey string + Rkey syntax.RecordKey +} + +var ErrKnotAnswer = errors.New("knot returned an invalid describeRepo answer") + +func Describe(ctx context.Context, httpClient *http.Client, knot repoident.KnotURL, repoDid repoident.RepoDid) (Result, error) { + client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} + out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) + if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { + return Result{}, fmt.Errorf("describeRepo on %s: %w (%v)", knot, xrpcErr, err) + } + if out.RepoDid != repoDid.String() { + return Result{}, fmt.Errorf("%w: knot %s returned repoDid %q, want %q", ErrKnotAnswer, knot, out.RepoDid, repoDid) + } + ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) + if err != nil { + return Result{}, fmt.Errorf("%w from knot %s: %w", ErrKnotAnswer, knot, err) + } + rkey, err := syntax.ParseRecordKey(out.Rkey) + if err != nil { + return Result{}, fmt.Errorf("%w: knot %s returned rkey %q: %w", ErrKnotAnswer, knot, out.Rkey, err) + } + return Result{RepoDid: repoDid, OwnerDid: ownerDid, KnotURL: knot, Rkey: rkey}, nil } type Verifier func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) @@ -54,86 +52,27 @@ const verifyTimeout = 10 * time.Second func New(resolver *idresolver.Resolver, dev bool) Verifier { - transport := &http.Transport{ - DialContext: safeDialer(dev).DialContext, - } - httpClient := &http.Client{ - Timeout: verifyTimeout, - Transport: transport, - } + httpClient := hostutil.SafeClient(dev, verifyTimeout) + policy := repoident.SchemeFor(dev) return func(ctx context.Context, repoDid repoident.RepoDid) (Result, error) { ctx, cancel := context.WithTimeout(ctx, verifyTimeout) defer cancel() - return resolveAndDescribe(ctx, resolver, httpClient, repoDid, dev) - } -} -func resolveAndDescribe( - ctx context.Context, - resolver *idresolver.Resolver, - httpClient *http.Client, - repoDid repoident.RepoDid, - dev bool, -) (Result, error) { - ident, err := resolver.ResolveIdent(ctx, repoDid.String()) - if err != nil { - return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) - } + ident, err := resolver.ResolveIdent(ctx, repoDid.String()) + if err != nil { + return Result{}, fmt.Errorf("resolve repoDid %s: %w", repoDid, err) + } - knot, err := ParseKnotEndpoint(ident.GetServiceEndpoint("atproto_pds"), dev) - if err != nil { - return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) - } + knot, err := repoident.KnotURLFromIdentity(ident, policy) + if err != nil { + return Result{}, fmt.Errorf("repoDid %s: %w", repoDid, err) + } - client := &indigoxrpc.Client{Host: knot.String(), Client: httpClient} - out, err := tangled.RepoDescribeRepo(ctx, client, repoDid.String()) - if xrpcErr := xrpcclient.HandleXrpcErr(err); xrpcErr != nil { - if errors.Is(xrpcErr, xrpcclient.ErrXrpcUnsupported) { + result, err := Describe(ctx, httpClient, knot, repoDid) + if errors.Is(err, xrpcclient.ErrXrpcUnsupported) { return Result{RepoDid: repoDid, KnotURL: knot}, nil } - return Result{}, fmt.Errorf("describeRepo on %s: %w", knot, xrpcErr) + return result, err } - - if out.RepoDid != repoDid.String() { - return Result{}, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knot, out.RepoDid, repoDid) - } - - ownerDid, err := repoident.NewOwnerDid(out.OwnerDid) - if err != nil { - return Result{}, fmt.Errorf("describeRepo on %s returned invalid ownerDid: %w", knot, err) - } - - return Result{ - RepoDid: repoDid, - OwnerDid: ownerDid, - KnotURL: knot, - Rkey: out.Rkey, - }, nil -} - -func safeDialer(dev bool) *net.Dialer { - d := &net.Dialer{ - Timeout: 5 * time.Second, - KeepAlive: 30 * time.Second, - } - if dev { - return d - } - d.Control = func(network, address string, _ syscall.RawConn) error { - host, _, err := net.SplitHostPort(address) - if err != nil { - return fmt.Errorf("invalid dial address %q: %w", address, err) - } - ip := net.ParseIP(host) - if ip == nil { - return fmt.Errorf("dial address %q did not resolve to IP", address) - } - if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() || - ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() { - return fmt.Errorf("refusing to dial %s: reserved or private address", ip) - } - return nil - } - return d } diff --git a/repoverify/verify_test.go b/repoverify/verify_test.go --- a/repoverify/verify_test.go +++ b/repoverify/verify_test.go @@ -1,49 +1,99 @@ package repoverify -import "testing" +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" -func TestParseKnotEndpoint_RejectsHttpInProd(t *testing.T) { - if _, err := ParseKnotEndpoint("http://knot.example", false); err == nil { - t.Error("http:// knot URL accepted in prod") - } + "github.com/bluesky-social/indigo/atproto/identity" + "tangled.org/core/api/tangled" + "tangled.org/core/idresolver" + "tangled.org/core/repoident" +) + +const ( + testRepoDid = repoident.RepoDid("did:plc:limpet") + testOwnerDid = "did:plc:akshay" +) + +func describeRepoServer(t *testing.T, describedRepoDid repoident.RepoDid) *httptest.Server { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/xrpc/"+tangled.RepoDescribeRepoNSID { + http.NotFound(w, r) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(tangled.RepoDescribeRepo_Output{ + RepoDid: describedRepoDid.String(), + OwnerDid: testOwnerDid, + Rkey: "3kkkkkkkkkkkk", + }) + })) + t.Cleanup(srv.Close) + return srv } -func TestParseKnotEndpoint_AllowsHttpInDev(t *testing.T) { - u, err := ParseKnotEndpoint("http://knot.example", true) +func verifyKnot(t *testing.T, knotURL string, dev bool) (Result, error) { + t.Helper() + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: &identity.Identity{ + Services: map[string]identity.ServiceEndpoint{ + repoident.KnotServiceID: {Type: repoident.KnotServiceType, URL: knotURL}, + }, + }}) + return New(resolver, dev)(context.Background(), testRepoDid) +} + +func TestNew_DevModeAcceptsHttpKnotEndpointAndStripsThePath(t *testing.T) { + srv := describeRepoServer(t, testRepoDid) + + result, err := verifyKnot(t, srv.URL+"/repo/m5326fp3qemiriiqypxv6rrhai", true) if err != nil { - t.Fatalf("dev mode should allow http: %v", err) + t.Fatalf("dev mode should accept an http knot endpoint: %v", err) } - if u.Host != "knot.example" { - t.Errorf("Host = %q, want knot.example", u.Host) + if result.OwnerDid.String() != testOwnerDid { + t.Errorf("OwnerDid = %q, want %q", result.OwnerDid, testOwnerDid) } -} - -func TestParseKnotEndpoint_RejectsUnsupportedScheme(t *testing.T) { - if _, err := ParseKnotEndpoint("ftp://knot.example", true); err == nil { - t.Error("ParseKnotEndpoint accepted ftp:// in dev") - } - if _, err := ParseKnotEndpoint("ftp://knot.example", false); err == nil { - t.Error("ParseKnotEndpoint accepted ftp:// in prod") + if result.KnotURL.String() != srv.URL { + t.Errorf("KnotURL = %q, want %q", result.KnotURL, srv.URL) } } -func TestParseKnotEndpoint_RejectsEmptyOrHostless(t *testing.T) { - cases := []string{"", "https://", "not a url at all"} - for _, raw := range cases { - t.Run(raw, func(t *testing.T) { - if _, err := ParseKnotEndpoint(raw, false); err == nil { - t.Errorf("ParseKnotEndpoint(%q) accepted bogus URL", raw) +func TestNew_Rejections(t *testing.T) { + target := describeRepoServer(t, testRepoDid) + otherRepo := describeRepoServer(t, "did:plc:anemone") + redirector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, target.URL+r.URL.Path, http.StatusFound) + })) + t.Cleanup(redirector.Close) + + cases := map[string]struct { + knotURL string + dev bool + want string + sentinel error + }{ + "http endpoint outside dev mode": {target.URL, false, "must use https", nil}, + "knot redirects elsewhere": {redirector.URL, true, "describeRepo on", nil}, + "identity declares no knot": {"", true, "", repoident.ErrNoKnotService}, + "describeRepo answers for another repo": {otherRepo.URL, true, `repoDid "did:plc:anemone"`, ErrKnotAnswer}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + _, err := verifyKnot(t, tc.knotURL, tc.dev) + if err == nil { + t.Fatalf("verify accepted a knot it should reject") + } + if !strings.Contains(err.Error(), tc.want) { + t.Errorf("error = %v, want one mentioning %q", err, tc.want) + } + if tc.sentinel != nil && !errors.Is(err, tc.sentinel) { + t.Errorf("error = %v, want one matching %v", err, tc.sentinel) } }) - } -} - -func TestParseKnotEndpoint_HostPreservesPort(t *testing.T) { - u, err := ParseKnotEndpoint("http://localhost:3000", true) - if err != nil { - t.Fatalf("ParseKnotEndpoint: %v", err) - } - if u.Host != "localhost:3000" { - t.Errorf("Host = %q, want localhost:3000", u.Host) } } diff --git a/spindle/server.go b/spindle/server.go --- a/spindle/server.go +++ b/spindle/server.go @@ -579,7 +579,7 @@ if err != nil { return nil, fmt.Errorf("verify sourceRepo %s: %w", repoDid, err) } - return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host, res.OwnerDid.String(), res.Rkey, repoDid.String()), nil + return s.buildTriggerRepoFrom(ctx, res.KnotURL.Host(), res.OwnerDid.String(), res.Rkey.String(), repoDid.String()), nil } // runPipeline compiles and enqueues the pipeline for the given revision. diff --git a/spindle/tapclient_test.go b/spindle/tapclient_test.go --- a/spindle/tapclient_test.go +++ b/spindle/tapclient_test.go @@ -21,26 +21,6 @@ "tangled.org/core/tapc" ) -type mockDirectory struct { - ident *identity.Identity -} - -func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { - return nil -} - func TestProcessRepo_MembershipCheck(t *testing.T) { d, e := newTestSpindleDB(t) @@ -323,7 +303,7 @@ DID: subjectDid, Handle: h, } - resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: mockIdent}) jc, jcerr := jetstream.NewJetstreamClient("", "", nil, nil, slog.Default(), nil, false, false) if jcerr != nil { diff --git a/knotmirror/xrpc/proxy.go b/knotmirror/xrpc/proxy.go --- a/knotmirror/xrpc/proxy.go +++ b/knotmirror/xrpc/proxy.go @@ -16,9 +16,12 @@ "github.com/bluesky-social/indigo/atproto/syntax" indigoxrpc "github.com/bluesky-social/indigo/xrpc" "github.com/go-git/go-git/v5/plumbing/filemode" + "github.com/samber/lo" "tangled.org/core/api/tangled" "tangled.org/core/knotmirror/db" "tangled.org/core/knotmirror/models" + "tangled.org/core/repoident" + "tangled.org/core/repoverify" ) var mirrorToKnotNSID = map[string]string{ @@ -49,31 +52,9 @@ repoIdentifier string } -// validateKnotURL ensures a knot base URL is safe to proxy to. -// It rejects URLs with path components, query strings, or fragments -// that could be used for path injection. -func validateKnotURL(raw string) (string, error) { - u, err := url.Parse(raw) - if err != nil { - return "", fmt.Errorf("invalid knot URL: %w", err) - } - if u.Scheme != "http" && u.Scheme != "https" { - return "", errors.New("knot URL must use http or https scheme") - } - if u.Path != "" && u.Path != "/" { - return "", fmt.Errorf("knot URL must not contain a path: %q", raw) - } - if u.RawQuery != "" || u.Fragment != "" { - return "", fmt.Errorf("knot URL must not contain query or fragment: %q", raw) - } - if u.User != nil { - return "", fmt.Errorf("knot URL must not contain userinfo: %q", raw) - } - // Strip trailing slash for consistent formatting - return strings.TrimRight(u.String(), "/"), nil -} - func (x *Xrpc) resolveKnot(ctx context.Context, repoDid syntax.DID) (*knotInfo, error) { + policy := repoident.SchemeFor(!x.cfg.KnotSSRF) + if repo, err := db.GetRepoByRepoDid(ctx, x.db, repoDid); err == nil && repo != nil { knotURL := repo.KnotDomain if !strings.Contains(repo.KnotDomain, "://") { @@ -81,52 +62,40 @@ knotURL = host.URL() } else { x.logger.Warn("repo is from unknown knot") - if x.cfg.KnotUseSSL { - knotURL = "https://" + knotURL - } else { - knotURL = "http://" + knotURL - } + knotURL = lo.Ternary(x.cfg.KnotUseSSL, "https://", "http://") + knotURL } } - knotURL, err = validateKnotURL(knotURL) + base, err := repoident.ParseKnotURL(knotURL, policy) if err != nil { return nil, err } - return &knotInfo{baseURL: knotURL, repoIdentifier: repo.RepoIdentifier()}, nil + return &knotInfo{baseURL: base.String(), repoIdentifier: repo.RepoIdentifier()}, nil } ident, err := x.resolver.ResolveIdent(ctx, repoDid.String()) if err != nil { return nil, fmt.Errorf("resolving repoDid %s: %w", repoDid, err) } - knotURL, err := validateKnotURL(ident.GetServiceEndpoint("atproto_pds")) + base, err := repoident.KnotURLFromIdentity(ident, policy) if err != nil { return nil, fmt.Errorf("repoDid %s: %w", repoDid, err) } + knotURL := base.String() - xrpcc := &indigoxrpc.Client{Host: knotURL, Client: x.httpClient} - out, err := tangled.RepoDescribeRepo(ctx, xrpcc, repoDid.String()) + described, err := repoverify.Describe(ctx, x.httpClient, base, repoident.RepoDid(repoDid)) + if errors.Is(err, repoverify.ErrKnotAnswer) { + return nil, err + } if err != nil { x.logger.Warn("describeRepo failed; serving without metadata upsert", "knot", knotURL, "repo", repoDid, "err", err) return &knotInfo{baseURL: knotURL, repoIdentifier: repoDid.String()}, nil } - if out.RepoDid != repoDid.String() { - return nil, fmt.Errorf("knot %s returned mismatched repoDid: got %q, want %q", knotURL, out.RepoDid, repoDid) - } - ownerDid, err := syntax.ParseDID(out.OwnerDid) - if err != nil { - return nil, fmt.Errorf("describeRepo on %s returned invalid ownerDid %q: %w", knotURL, out.OwnerDid, err) - } - rkey, err := syntax.ParseRecordKey(out.Rkey) - if err != nil { - return nil, fmt.Errorf("describeRepo on %s returned invalid rkey %q: %w", knotURL, out.Rkey, err) - } go func() { pending := &models.Repo{ - Did: ownerDid, - Rkey: rkey, - Name: string(rkey), + Did: syntax.DID(described.OwnerDid), + Rkey: described.Rkey, + Name: string(described.Rkey), KnotDomain: knotURL, RepoDid: repoDid, State: models.RepoStatePending, diff --git a/knotserver/keys/keys_test.go b/knotserver/keys/keys_test.go --- a/knotserver/keys/keys_test.go +++ b/knotserver/keys/keys_test.go @@ -3,7 +3,6 @@ import ( "context" "encoding/json" - "errors" "net/http" "net/http/httptest" "path/filepath" @@ -14,6 +13,7 @@ "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" "tangled.org/core/api/tangled" + "tangled.org/core/idresolver" "tangled.org/core/knotserver/db" ) @@ -32,7 +32,7 @@ }) defer srv.Close() - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { t.Fatalf("FetchAndStore: %v", err) } @@ -54,7 +54,7 @@ }) defer srv.Close() - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { t.Fatalf("FetchAndStore: %v", err) } @@ -82,7 +82,7 @@ }) defer srv.Close() - if err := FetchAndStore(context.Background(), fakeDirectory{pdsURL: srv.URL}, store, didBoltless); err != nil { + if err := FetchAndStore(context.Background(), pdsDirectory(srv.URL), store, didBoltless); err != nil { t.Fatalf("FetchAndStore: %v", err) } @@ -173,27 +173,10 @@ })) } -type fakeDirectory struct { - pdsURL string -} - -func (f fakeDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { - return &identity.Identity{ - DID: did, +func pdsDirectory(url string) idresolver.MockDirectory { + return idresolver.MockDirectory{Ident: &identity.Identity{ Services: map[string]identity.ServiceEndpoint{ - "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: f.pdsURL}, + "atproto_pds": {Type: "AtprotoPersonalDataServer", URL: url}, }, - }, nil -} - -func (f fakeDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { - return nil, errors.New("LookupHandle unused in tests") -} - -func (f fakeDirectory) Lookup(ctx context.Context, atid syntax.AtIdentifier) (*identity.Identity, error) { - return nil, errors.New("Lookup unused in tests") -} - -func (f fakeDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { - return nil + }} } diff --git a/knotserver/repodid/repodid.go b/knotserver/repodid/repodid.go --- a/knotserver/repodid/repodid.go +++ b/knotserver/repodid/repodid.go @@ -9,6 +9,7 @@ atcrypto "github.com/bluesky-social/indigo/atproto/atcrypto" "github.com/did-method-plc/go-didplc/didplc" "tangled.org/core/idresolver" + "tangled.org/core/repoident" ) type PreparedDID struct { @@ -47,8 +48,8 @@ }, AlsoKnownAs: []string{}, Services: map[string]didplc.OpService{ - "atproto_pds": { - Type: "AtprotoPersonalDataServer", + repoident.LegacyKnotServiceID: { + Type: repoident.LegacyKnotServiceType, Endpoint: knotServiceUrl, }, }, @@ -107,11 +108,18 @@ return fmt.Errorf("resolving did:web document: %w", err) } - knotEndpoint := ident.GetServiceEndpoint("atproto_pds") - if strings.TrimRight(knotEndpoint, "/") != strings.TrimRight(knotServiceUrl, "/") { + knotEndpoint, err := repoident.KnotURLFromIdentity(ident, repoident.AllowHTTP) + if err != nil { + return fmt.Errorf("did:web document: %w", err) + } + expected, err := repoident.ParseKnotURL(knotServiceUrl, repoident.AllowHTTP) + if err != nil { + return fmt.Errorf("knot service URL %q: %w", knotServiceUrl, err) + } + if knotEndpoint != expected { return fmt.Errorf( - "did:web atproto_pds service endpoint %q does not match this knot %q", - knotEndpoint, knotServiceUrl, + "did:web knot service endpoint %q doesn't match this knot %q", + knotEndpoint, expected, ) } diff --git a/spindle/xrpc/xrpc_test.go b/spindle/xrpc/xrpc_test.go --- a/spindle/xrpc/xrpc_test.go +++ b/spindle/xrpc/xrpc_test.go @@ -232,26 +232,6 @@ } } -type mockDirectory struct { - ident *identity.Identity -} - -func (m *mockDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) LookupHandle(ctx context.Context, handle syntax.Handle) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) Lookup(ctx context.Context, id syntax.AtIdentifier) (*identity.Identity, error) { - return m.ident, nil -} - -func (m *mockDirectory) Purge(ctx context.Context, id syntax.AtIdentifier) error { - return nil -} - func TestSecrets_RBAC(t *testing.T) { d, e := newTestXrpcDB(t) @@ -322,7 +302,7 @@ }, } - resolver := idresolver.NewMockResolver(&mockDirectory{ident: mockIdent}) + resolver := idresolver.NewMockResolver(idresolver.MockDirectory{Ident: mockIdent}) x := &Xrpc{ Logger: slog.Default(), -- tangled.sh