this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

identity: polish for initial review

+144 -92
+66 -43
atproto/identity/cache_catalog.go
··· 9 9 "github.com/bluesky-social/indigo/atproto/syntax" 10 10 ) 11 11 12 - // TODO: refactor this to wrap a regular Catalog. have it always update both handle and identity maps together. 13 12 type CacheCatalog struct { 14 13 HitTTL time.Duration 15 14 ErrTTL time.Duration 16 - PLCURL string 15 + Inner Catalog 17 16 mutex sync.RWMutex 18 17 handleCache map[syntax.Handle]HandleEntry 19 18 identityCache map[syntax.DID]IdentityEntry ··· 33 32 34 33 var _ Catalog = (*CacheCatalog)(nil) 35 34 36 - func NewCacheCatalog(plcURL string) CacheCatalog { 37 - // TODO: these are kind of arbitrary default values 35 + func NewCacheCatalog(inner Catalog) CacheCatalog { 36 + // TODO: these are kind of arbitrary default values... 38 37 hitTTL, err := time.ParseDuration("1h") 39 38 if err != nil { 40 39 panic(err) ··· 46 45 return CacheCatalog{ 47 46 HitTTL: hitTTL, 48 47 ErrTTL: errTTL, 49 - PLCURL: plcURL, 48 + Inner: inner, 50 49 handleCache: make(map[syntax.Handle]HandleEntry, 10), 51 50 identityCache: make(map[syntax.DID]IdentityEntry, 10), 52 51 } 53 52 } 54 53 54 + func (c *CacheCatalog) IsHandleStale(e *HandleEntry) bool { 55 + if e.Err == nil && time.Since(e.Updated) > c.HitTTL { 56 + return true 57 + } 58 + if e.Err != nil && time.Since(e.Updated) > c.ErrTTL { 59 + return true 60 + } 61 + return false 62 + } 63 + 64 + func (c *CacheCatalog) IsIdentityStale(e *IdentityEntry) bool { 65 + if e.Err == nil && time.Since(e.Updated) > c.HitTTL { 66 + return true 67 + } 68 + if e.Err != nil && time.Since(e.Updated) > c.ErrTTL { 69 + return true 70 + } 71 + return false 72 + } 73 + 55 74 func (c *CacheCatalog) updateHandle(ctx context.Context, h syntax.Handle) (*HandleEntry, error) { 56 - did, err := ResolveHandle(ctx, h) 57 - entry := HandleEntry{ 75 + ident, err := c.Inner.LookupHandle(ctx, h) 76 + if err != nil { 77 + he := HandleEntry{ 78 + Updated: time.Now(), 79 + DID: "", 80 + Err: err, 81 + } 82 + c.mutex.Lock() 83 + c.handleCache[h] = he 84 + c.mutex.Unlock() 85 + return &he, nil 86 + } 87 + 88 + entry := IdentityEntry{ 89 + Updated: time.Now(), 90 + Identity: ident, 91 + Err: nil, 92 + } 93 + he := HandleEntry{ 58 94 Updated: time.Now(), 59 - DID: did, 60 - Err: err, 95 + DID: ident.DID, 96 + Err: nil, 61 97 } 98 + 62 99 c.mutex.Lock() 63 - c.handleCache[h] = entry 100 + c.identityCache[ident.DID] = entry 101 + c.handleCache[ident.Handle] = he 64 102 c.mutex.Unlock() 65 - return &entry, nil 103 + return &he, nil 66 104 } 67 105 68 106 func (c *CacheCatalog) ResolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { ··· 80 118 } else { 81 119 entry = &eObj 82 120 } 83 - if (entry.Err == nil && time.Since(entry.Updated) > c.HitTTL) || (entry.Err != nil && time.Since(entry.Updated) > c.ErrTTL) { 121 + if c.IsHandleStale(entry) { 84 122 entry, err = c.updateHandle(ctx, h) 85 123 if err != nil { 86 124 return "", err ··· 89 127 return entry.DID, entry.Err 90 128 } 91 129 92 - func (c *CacheCatalog) getIdentity(ctx context.Context, did syntax.DID) (*Identity, error) { 93 - doc, err := ResolveDID(ctx, did) 94 - if err != nil { 95 - return nil, err 96 - } 97 - ident := ParseIdentity(doc) 98 - declared, err := ident.DeclaredHandle() 99 - if err != nil { 100 - return nil, err 101 - } 102 - resolvedDID, err := c.ResolveHandle(ctx, declared) 103 - if err != nil { 104 - return nil, err 105 - } 106 - if resolvedDID == did { 107 - ident.Handle = declared 108 - } 109 - 110 - // optimistic caching of public key 111 - pk, err := ident.PublicKey() 112 - if nil == err { 113 - ident.ParsedPublicKey = pk 114 - } 115 - return &ident, nil 116 - } 117 - 118 - func (c *CacheCatalog) updateIdentity(ctx context.Context, did syntax.DID) (*IdentityEntry, error) { 119 - ident, err := c.getIdentity(ctx, did) 130 + func (c *CacheCatalog) updateDID(ctx context.Context, did syntax.DID) (*IdentityEntry, error) { 131 + ident, err := c.Inner.LookupDID(ctx, did) 120 132 entry := IdentityEntry{ 121 133 Updated: time.Now(), 122 134 Identity: ident, 123 135 Err: err, 124 136 } 137 + var he *HandleEntry 138 + if err == nil && !ident.Handle.IsInvalidHandle() { 139 + he = &HandleEntry{ 140 + Updated: time.Now(), 141 + DID: did, 142 + Err: nil, 143 + } 144 + } 125 145 126 146 c.mutex.Lock() 127 147 c.identityCache[did] = entry 148 + if he != nil { 149 + c.handleCache[ident.Handle] = *he 150 + } 128 151 c.mutex.Unlock() 129 152 return &entry, nil 130 153 } ··· 137 160 c.mutex.RUnlock() 138 161 139 162 if !ok { 140 - entry, err = c.updateIdentity(ctx, did) 163 + entry, err = c.updateDID(ctx, did) 141 164 if err != nil { 142 165 return nil, err 143 166 } 144 167 } else { 145 168 entry = &eObj 146 169 } 147 - if (entry.Err == nil && time.Since(entry.Updated) > c.HitTTL) || (entry.Err != nil && time.Since(entry.Updated) > c.ErrTTL) { 148 - entry, err = c.updateIdentity(ctx, did) 170 + if c.IsIdentityStale(entry) { 171 + entry, err = c.updateDID(ctx, did) 149 172 if err != nil { 150 173 return nil, err 151 174 }
+7 -4
atproto/identity/cmd/atp-id/main.go
··· 38 38 } 39 39 40 40 func runLookup(cctx *cli.Context) error { 41 + ctx := context.Background() 41 42 s := cctx.Args().First() 42 43 if s == "" { 43 44 return fmt.Errorf("need to provide identifier as an argument") ··· 49 50 } 50 51 fmt.Printf("valid at-identifier syntax: %s\n", id) 51 52 52 - ncat := identity.NewNaiveCatalog("https://plc.directory") 53 + ncat := identity.NewBasicCatalog("https://plc.directory") 53 54 54 - acc, err := ncat.Lookup(context.TODO(), *id) 55 + acc, err := ncat.Lookup(ctx, *id) 55 56 if err != nil { 56 57 return err 57 58 } ··· 60 61 } 61 62 62 63 func runResolveHandle(cctx *cli.Context) error { 64 + ctx := context.Background() 63 65 s := cctx.Args().First() 64 66 if s == "" { 65 67 return fmt.Errorf("need to provide handle as an argument") ··· 71 73 } 72 74 fmt.Printf("valid handle syntax: %s\n", handle) 73 75 74 - did, err := identity.ResolveHandle(context.TODO(), handle) 76 + did, err := identity.ResolveHandle(ctx, handle) 75 77 if err != nil { 76 78 return err 77 79 } ··· 80 82 } 81 83 82 84 func runResolveDID(cctx *cli.Context) error { 85 + ctx := context.Background() 83 86 s := cctx.Args().First() 84 87 if s == "" { 85 88 fmt.Println("need to provide DID as an argument") ··· 93 96 } 94 97 fmt.Printf("valid DID syntax: %s\n", did) 95 98 96 - doc, err := identity.ResolveDID(context.TODO(), did) 99 + doc, err := identity.DefaultResolveDID(ctx, did) 97 100 if err != nil { 98 101 return err 99 102 }
+5 -9
atproto/identity/did.go
··· 31 31 ServiceEndpoint string `json:"serviceEndpoint"` 32 32 } 33 33 34 - // Indicates that resolution process completed successfully, but the DID does not exist. 35 - var ErrDIDNotFound = errors.New("DID not found") 36 - 37 34 // 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 38 - func ResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) { 35 + func DefaultResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) { 39 36 switch did.Method() { 40 37 case "web": 41 38 return ResolveDIDWeb(ctx, did) 42 39 case "plc": 43 - return ResolveDIDPLC(ctx, did) 40 + return ResolveDIDPLC(ctx, DefaultPLCURL, did) 44 41 default: 45 42 return nil, fmt.Errorf("DID method not supported: %s", did.Method()) 46 43 } ··· 75 72 if resp.StatusCode == 404 { 76 73 return nil, ErrDIDNotFound 77 74 } 78 - // TODO: HTTP redirects 79 75 if resp.StatusCode != 200 { 80 76 return nil, fmt.Errorf("failed did:web well-known fetch, HTTP status: %d", resp.StatusCode) 81 77 } ··· 87 83 return &doc, nil 88 84 } 89 85 90 - func ResolveDIDPLC(ctx context.Context, did syntax.DID) (*DIDDocument, error) { 91 - // TODO: configurable PLC hostname 86 + // plcURL should have URL method, hostname, and optional port; it should not have a path or trailing slash 87 + func ResolveDIDPLC(ctx context.Context, plcURL string, did syntax.DID) (*DIDDocument, error) { 92 88 if did.Method() != "plc" { 93 89 return nil, fmt.Errorf("expected a did:plc, got: %s", did) 94 90 } 95 91 96 - resp, err := http.Get("https://plc.directory/" + did.String()) 92 + resp, err := http.Get(plcURL + "/" + did.String()) 97 93 if err != nil { 98 94 return nil, fmt.Errorf("failed did:plc directory resolution: %w", err) 99 95 }
+3 -1
atproto/identity/doc.go
··· 1 1 /* 2 - Package identity provides types and routines for resolving handles and DIDs from the network 2 + Package identity provides types and routines for resolving handles and DIDs from the network 3 + 4 + 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. 3 5 4 6 Much of the implementation of this SDK is based on existing code in indigo:api/extra.go 5 7 */
+2 -5
atproto/identity/handle.go
··· 12 12 "github.com/bluesky-social/indigo/atproto/syntax" 13 13 ) 14 14 15 - // Indicates that resolution process completed successfully, but handle does not exist. 16 - var ErrHandleNotFound = errors.New("handle not found") 17 - 18 15 // Does not cross-verify, just does the handle resolution step. 19 16 func ResolveHandleDNS(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 20 17 // TODO: timeout 21 - // TODO: mechanism to control resolution; context? separate method? 18 + // TODO: mechanism to control NDS better; context? separate method? 22 19 23 20 res, err := net.LookupTXT("_atproto." + handle.String()) 24 21 // look for NXDOMAIN ··· 46 43 } 47 44 48 45 func ResolveHandleWellKnown(ctx context.Context, handle syntax.Handle) (syntax.DID, error) { 49 - // TODO: could pull a client or transport from context? 50 46 // TODO: timeout 47 + // TODO: could pull a client or transport from context? 51 48 c := http.DefaultClient 52 49 53 50 req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/.well-known/atproto-did", handle), nil)
+28 -11
atproto/identity/identity.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "errors" 5 6 "fmt" 6 7 "strings" 7 8 ··· 11 12 "github.com/mr-tron/base58" 12 13 ) 13 14 14 - // API for doing account lookups by DID or handle, with bi-directional verification handled automatically. 15 + // 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 15 16 // 16 - // 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. 17 + // 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. 17 18 // 18 - // Some example implementations of this interface would be: 19 - // - naive direct resolution on every call 19 + // Some example implementations of this interface could be: 20 + // - basic direct resolution on every call 21 + // - local in-memory caching layer to reduce network hits 20 22 // - API client, which just makes requests to PDS (or other remote service) 21 - // - simple in-memory caching wrapper layer to reduce network hits 22 - // - services with backing datastore to do sophisticated caching, TTL, auto-refresh, etc 23 + // - client for shared network cache (eg, Redis) 23 24 type Catalog interface { 24 25 LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) 25 26 LookupDID(ctx context.Context, d syntax.DID) (*Identity, error) ··· 27 28 // TODO: add "flush" methods to purge caches? 28 29 } 29 30 31 + // Indicates that resolution process completed successfully, but handle does not exist. 32 + var ErrHandleNotFound = errors.New("handle not found") 33 + 34 + // 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. 35 + var ErrHandleNotValid = errors.New("handle resolves to DID with different handle") 36 + 37 + // Indicates that resolution process completed successfully, but the DID does not exist. 38 + var ErrDIDNotFound = errors.New("DID not found") 39 + 30 40 var DefaultPLCURL = "https://plc.directory" 31 41 42 + // Returns a reasonable default Catalog implementation for most use cases 32 43 func DefaultCatalog() Catalog { 33 - cat := NewCacheCatalog(DefaultPLCURL) 34 - return &cat 44 + naive := NewBasicCatalog(DefaultPLCURL) 45 + cached := NewCacheCatalog(&naive) 46 + return &cached 35 47 } 36 - 37 - // 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 38 48 39 49 // Represents an atproto identity. Could be a regular user account, or a service account (eg, feed generator) 40 50 type Identity struct { ··· 52 62 // TODO: this doesn't preserve order (doesn't round-trip) 53 63 Keys map[string]Key 54 64 55 - // If a valid atproto repo signing public key was parsed, it can be cached here. This is nullable/optional. 65 + // 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. 56 66 ParsedPublicKey crypto.PublicKey 57 67 } 58 68 ··· 112 122 } 113 123 } 114 124 125 + // Identifiers the atproto repo signing public key, specifically, out of any keys associated with this identity. 126 + // 127 + // Returns an error if there is no key. Note that 'crypto.PublicKey' is an interface, not a concrete type. 115 128 func (i *Identity) PublicKey() (crypto.PublicKey, error) { 116 129 if i.ParsedPublicKey != nil { 117 130 return i.ParsedPublicKey, nil ··· 149 162 } 150 163 } 151 164 165 + // 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. 152 166 func (i *Identity) PDSEndpoint() string { 153 167 if i.Services == nil { 154 168 return "" ··· 160 174 return atp.URL 161 175 } 162 176 177 + // 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. 178 + // 179 + // 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. 163 180 func (i *Identity) DeclaredHandle() (syntax.Handle, error) { 164 181 for _, u := range i.AlsoKnownAs { 165 182 if strings.HasPrefix(u, "at://") && len(u) > len("at://") {
+7 -6
atproto/identity/live_test.go
··· 12 12 // NOTE: this hits the open internet! marked as skip below by default 13 13 func testCatalogLive(t *testing.T, c Catalog) { 14 14 assert := assert.New(t) 15 - ctx := context.TODO() 15 + ctx := context.Background() 16 16 17 17 handle := syntax.Handle("atproto.com") 18 18 did := syntax.DID("did:plc:ewvi7nxzyoun6zhxrhs64oiz") ··· 49 49 assert.Error(err) 50 50 } 51 51 52 - func TestNaiveCatalog(t *testing.T) { 53 - t.Skip("skipping live network test") 54 - c := NewNaiveCatalog(DefaultPLCURL) 52 + func TestBasicCatalog(t *testing.T) { 53 + // XXX: t.Skip("skipping live network test") 54 + c := NewBasicCatalog(DefaultPLCURL) 55 55 testCatalogLive(t, &c) 56 56 } 57 57 58 58 func TestCacheCatalog(t *testing.T) { 59 - t.Skip("skipping live network test") 60 - c := NewCacheCatalog(DefaultPLCURL) 59 + // XXX: t.Skip("skipping live network test") 60 + inner := NewBasicCatalog(DefaultPLCURL) 61 + c := NewCacheCatalog(&inner) 61 62 for i := 0; i < 3; i = i + 1 { 62 63 testCatalogLive(t, &c) 63 64 }
+1 -1
atproto/identity/mock_catalog_test.go
··· 12 12 func TestMockCatalog(t *testing.T) { 13 13 var err error 14 14 assert := assert.New(t) 15 - ctx := context.TODO() 15 + ctx := context.Background() 16 16 c := NewMockCatalog() 17 17 id1 := Identity{ 18 18 DID: syntax.DID("did:plc:abc111"),
+25 -12
atproto/identity/naive_catalog.go atproto/identity/basic_catalog.go
··· 7 7 "github.com/bluesky-social/indigo/atproto/syntax" 8 8 ) 9 9 10 - type NaiveCatalog struct { 11 - // TODO: actually wire through use of PLC host 12 - PLCHost string 10 + type BasicCatalog struct { 11 + PLCURL string 13 12 } 14 13 15 - var _ Catalog = (*NaiveCatalog)(nil) 14 + var _ Catalog = (*BasicCatalog)(nil) 15 + 16 + func NewBasicCatalog(plcURL string) BasicCatalog { 17 + if plcURL == "" { 18 + plcURL = DefaultPLCURL 19 + } 20 + return BasicCatalog{ 21 + PLCURL: plcURL, 22 + } 23 + } 16 24 17 - func NewNaiveCatalog(plcHost string) NaiveCatalog { 18 - return NaiveCatalog{ 19 - PLCHost: plcHost, 25 + func (c *BasicCatalog) ResolveDID(ctx context.Context, did syntax.DID) (*DIDDocument, error) { 26 + switch did.Method() { 27 + case "web": 28 + return ResolveDIDWeb(ctx, did) 29 + case "plc": 30 + return ResolveDIDPLC(ctx, c.PLCURL, did) 31 + default: 32 + return nil, fmt.Errorf("DID method not supported: %s", did.Method()) 20 33 } 21 34 } 22 35 23 - func (c *NaiveCatalog) LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) { 36 + func (c *BasicCatalog) LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) { 24 37 did, err := ResolveHandle(ctx, h) 25 38 if err != nil { 26 39 return nil, err 27 40 } 28 - doc, err := ResolveDID(ctx, did) 41 + doc, err := c.ResolveDID(ctx, did) 29 42 if err != nil { 30 43 return nil, err 31 44 } ··· 47 60 return &ident, nil 48 61 } 49 62 50 - func (c *NaiveCatalog) LookupDID(ctx context.Context, did syntax.DID) (*Identity, error) { 51 - doc, err := ResolveDID(ctx, did) 63 + func (c *BasicCatalog) LookupDID(ctx context.Context, did syntax.DID) (*Identity, error) { 64 + doc, err := c.ResolveDID(ctx, did) 52 65 if err != nil { 53 66 return nil, err 54 67 } ··· 73 86 return &ident, nil 74 87 } 75 88 76 - func (c *NaiveCatalog) Lookup(ctx context.Context, a syntax.AtIdentifier) (*Identity, error) { 89 + func (c *BasicCatalog) Lookup(ctx context.Context, a syntax.AtIdentifier) (*Identity, error) { 77 90 handle, err := a.AsHandle() 78 91 if err == nil { 79 92 return c.LookupHandle(ctx, handle)