···3838}
39394040func runLookup(cctx *cli.Context) error {
4141+ ctx := context.Background()
4142 s := cctx.Args().First()
4243 if s == "" {
4344 return fmt.Errorf("need to provide identifier as an argument")
···4950 }
5051 fmt.Printf("valid at-identifier syntax: %s\n", id)
51525252- ncat := identity.NewNaiveCatalog("https://plc.directory")
5353+ ncat := identity.NewBasicCatalog("https://plc.directory")
53545454- acc, err := ncat.Lookup(context.TODO(), *id)
5555+ acc, err := ncat.Lookup(ctx, *id)
5556 if err != nil {
5657 return err
5758 }
···6061}
61626263func runResolveHandle(cctx *cli.Context) error {
6464+ ctx := context.Background()
6365 s := cctx.Args().First()
6466 if s == "" {
6567 return fmt.Errorf("need to provide handle as an argument")
···7173 }
7274 fmt.Printf("valid handle syntax: %s\n", handle)
73757474- did, err := identity.ResolveHandle(context.TODO(), handle)
7676+ did, err := identity.ResolveHandle(ctx, handle)
7577 if err != nil {
7678 return err
7779 }
···8082}
81838284func runResolveDID(cctx *cli.Context) error {
8585+ ctx := context.Background()
8386 s := cctx.Args().First()
8487 if s == "" {
8588 fmt.Println("need to provide DID as an argument")
···9396 }
9497 fmt.Printf("valid DID syntax: %s\n", did)
95989696- doc, err := identity.ResolveDID(context.TODO(), did)
9999+ doc, err := identity.DefaultResolveDID(ctx, did)
97100 if err != nil {
98101 return err
99102 }
+5-9
atproto/identity/did.go
···3131 ServiceEndpoint string `json:"serviceEndpoint"`
3232}
33333434-// Indicates that resolution process completed successfully, but the DID does not exist.
3535-var ErrDIDNotFound = errors.New("DID not found")
3636-3734// WARNING: this does *not* bi-directionally verify account metadata; it only implements direct DID-to-DID-document lookup for the supported DID methods, and parses the resulting DID Doc into an Identity struct
3838-func ResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
3535+func DefaultResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
3936 switch did.Method() {
4037 case "web":
4138 return ResolveDIDWeb(ctx, did)
4239 case "plc":
4343- return ResolveDIDPLC(ctx, did)
4040+ return ResolveDIDPLC(ctx, DefaultPLCURL, did)
4441 default:
4542 return nil, fmt.Errorf("DID method not supported: %s", did.Method())
4643 }
···7572 if resp.StatusCode == 404 {
7673 return nil, ErrDIDNotFound
7774 }
7878- // TODO: HTTP redirects
7975 if resp.StatusCode != 200 {
8076 return nil, fmt.Errorf("failed did:web well-known fetch, HTTP status: %d", resp.StatusCode)
8177 }
···8783 return &doc, nil
8884}
89859090-func ResolveDIDPLC(ctx context.Context, did syntax.DID) (*DIDDocument, error) {
9191- // TODO: configurable PLC hostname
8686+// plcURL should have URL method, hostname, and optional port; it should not have a path or trailing slash
8787+func ResolveDIDPLC(ctx context.Context, plcURL string, did syntax.DID) (*DIDDocument, error) {
9288 if did.Method() != "plc" {
9389 return nil, fmt.Errorf("expected a did:plc, got: %s", did)
9490 }
95919696- resp, err := http.Get("https://plc.directory/" + did.String())
9292+ resp, err := http.Get(plcURL + "/" + did.String())
9793 if err != nil {
9894 return nil, fmt.Errorf("failed did:plc directory resolution: %w", err)
9995 }
+3-1
atproto/identity/doc.go
···11/*
22- Package identity provides types and routines for resolving handles and DIDs from the network
22+Package identity provides types and routines for resolving handles and DIDs from the network
33+44+The two main abstractions are a Catalog interface for identity service implementations, and an Identity structure which represents core identity information relevant to atproto. The Catalog interface can be nested, somewhat like HTTP middleware, to provide caching, observability, or other bespoke needs in more complex systems.
3546Much of the implementation of this SDK is based on existing code in indigo:api/extra.go
57*/
+2-5
atproto/identity/handle.go
···1212 "github.com/bluesky-social/indigo/atproto/syntax"
1313)
14141515-// Indicates that resolution process completed successfully, but handle does not exist.
1616-var ErrHandleNotFound = errors.New("handle not found")
1717-1815// Does not cross-verify, just does the handle resolution step.
1916func ResolveHandleDNS(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
2017 // TODO: timeout
2121- // TODO: mechanism to control resolution; context? separate method?
1818+ // TODO: mechanism to control NDS better; context? separate method?
22192320 res, err := net.LookupTXT("_atproto." + handle.String())
2421 // look for NXDOMAIN
···4643}
47444845func ResolveHandleWellKnown(ctx context.Context, handle syntax.Handle) (syntax.DID, error) {
4949- // TODO: could pull a client or transport from context?
5046 // TODO: timeout
4747+ // TODO: could pull a client or transport from context?
5148 c := http.DefaultClient
52495350 req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/.well-known/atproto-did", handle), nil)
+28-11
atproto/identity/identity.go
···2233import (
44 "context"
55+ "errors"
56 "fmt"
67 "strings"
78···1112 "github.com/mr-tron/base58"
1213)
13141414-// API for doing account lookups by DID or handle, with bi-directional verification handled automatically.
1515+// API for doing account lookups by DID or handle, with bi-directional verification handled automatically. Almost all atproto services and clients should use an implementation of this interface instead of resolving handles or DIDs separately
1516//
1616-// Handles which fail to resolve or don't match DID alsoKnownAs are an error. DIDs which resolve but the handle does not resolve back to the DID return an Identity where the Handle is the special `handle.invalid` value.
1717+// Handles which fail to resolve, or don't match DID alsoKnownAs, are an error. DIDs which resolve but the handle does not resolve back to the DID return an Identity where the Handle is the special `handle.invalid` value.
1718//
1818-// Some example implementations of this interface would be:
1919-// - naive direct resolution on every call
1919+// Some example implementations of this interface could be:
2020+// - basic direct resolution on every call
2121+// - local in-memory caching layer to reduce network hits
2022// - API client, which just makes requests to PDS (or other remote service)
2121-// - simple in-memory caching wrapper layer to reduce network hits
2222-// - services with backing datastore to do sophisticated caching, TTL, auto-refresh, etc
2323+// - client for shared network cache (eg, Redis)
2324type Catalog interface {
2425 LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error)
2526 LookupDID(ctx context.Context, d syntax.DID) (*Identity, error)
···2728 // TODO: add "flush" methods to purge caches?
2829}
29303131+// Indicates that resolution process completed successfully, but handle does not exist.
3232+var ErrHandleNotFound = errors.New("handle not found")
3333+3434+// Indicates that handle and DID resolved, but handle points to a DID with a different handle. This is only returned when looking up a handle, not when looking up a DID.
3535+var ErrHandleNotValid = errors.New("handle resolves to DID with different handle")
3636+3737+// Indicates that resolution process completed successfully, but the DID does not exist.
3838+var ErrDIDNotFound = errors.New("DID not found")
3939+3040var DefaultPLCURL = "https://plc.directory"
31414242+// Returns a reasonable default Catalog implementation for most use cases
3243func DefaultCatalog() Catalog {
3333- cat := NewCacheCatalog(DefaultPLCURL)
3434- return &cat
4444+ naive := NewBasicCatalog(DefaultPLCURL)
4545+ cached := NewCacheCatalog(&naive)
4646+ return &cached
3547}
3636-3737-// TODO: DIDHistory() helper, returns log of declared handle, PDS location, and public key? or maybe this is something best left to did-method-plc helper library
38483949// Represents an atproto identity. Could be a regular user account, or a service account (eg, feed generator)
4050type Identity struct {
···5262 // TODO: this doesn't preserve order (doesn't round-trip)
5363 Keys map[string]Key
54645555- // If a valid atproto repo signing public key was parsed, it can be cached here. This is nullable/optional.
6565+ // If a valid atproto repo signing public key was parsed, it can be cached here. This is nullable/optional. Calling code should call PublicKey() instead of accessing this member.
5666 ParsedPublicKey crypto.PublicKey
5767}
5868···112122 }
113123}
114124125125+// Identifiers the atproto repo signing public key, specifically, out of any keys associated with this identity.
126126+//
127127+// Returns an error if there is no key. Note that 'crypto.PublicKey' is an interface, not a concrete type.
115128func (i *Identity) PublicKey() (crypto.PublicKey, error) {
116129 if i.ParsedPublicKey != nil {
117130 return i.ParsedPublicKey, nil
···149162 }
150163}
151164165165+// The home PDS endpoint for this account, if one is included in identity metadata (returns empty string if not found). The endpoint will be an HTTP URL with method, hostname, and optional port, but no path segments.
152166func (i *Identity) PDSEndpoint() string {
153167 if i.Services == nil {
154168 return ""
···160174 return atp.URL
161175}
162176177177+// Returns an atproto handle from the alsoKnownAs URI list for this identifier. Returns an error if there is no handle, or if an at:// URI failes to parse as a handle.
178178+//
179179+// Note that this handle is *not* necessarily to be trusted, as it may not have been bi-directionally verified. The 'Handle' field on the 'Identity' should contain either a verified handle, or the special 'handle.invalid' indicator value.
163180func (i *Identity) DeclaredHandle() (syntax.Handle, error) {
164181 for _, u := range i.AlsoKnownAs {
165182 if strings.HasPrefix(u, "at://") && len(u) > len("at://") {
+7-6
atproto/identity/live_test.go
···1212// NOTE: this hits the open internet! marked as skip below by default
1313func testCatalogLive(t *testing.T, c Catalog) {
1414 assert := assert.New(t)
1515- ctx := context.TODO()
1515+ ctx := context.Background()
16161717 handle := syntax.Handle("atproto.com")
1818 did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz")
···4949 assert.Error(err)
5050}
51515252-func TestNaiveCatalog(t *testing.T) {
5353- t.Skip("skipping live network test")
5454- c := NewNaiveCatalog(DefaultPLCURL)
5252+func TestBasicCatalog(t *testing.T) {
5353+ // XXX: t.Skip("skipping live network test")
5454+ c := NewBasicCatalog(DefaultPLCURL)
5555 testCatalogLive(t, &c)
5656}
57575858func TestCacheCatalog(t *testing.T) {
5959- t.Skip("skipping live network test")
6060- c := NewCacheCatalog(DefaultPLCURL)
5959+ // XXX: t.Skip("skipping live network test")
6060+ inner := NewBasicCatalog(DefaultPLCURL)
6161+ c := NewCacheCatalog(&inner)
6162 for i := 0; i < 3; i = i + 1 {
6263 testCatalogLive(t, &c)
6364 }