diff --git a/atproto/identity/directory.go b/atproto/identity/directory.go new file mode 100644 index 000000000..b0d2b2f10 --- /dev/null +++ b/atproto/identity/directory.go @@ -0,0 +1,81 @@ +package identity + +import ( + "context" + "errors" + "net" + "net/http" + "time" + + "github.com/bluesky-social/indigo/atproto/syntax" +) + +// 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 +// +// 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. +// +// Some example implementations of this interface could be: +// - basic direct resolution on every call +// - local in-memory caching layer to reduce network hits +// - API client, which just makes requests to PDS (or other remote service) +// - client for shared network cache (eg, Redis) +type Directory interface { + LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) + LookupDID(ctx context.Context, d syntax.DID) (*Identity, error) + Lookup(ctx context.Context, i syntax.AtIdentifier) (*Identity, error) + + // Flushes any cache of the indicated identifier. If directory is not using caching, can ignore this. + Purge(ctx context.Context, i syntax.AtIdentifier) error +} + +// Indicates that handle resolution failed. A wrapped error may provide more context. This is only returned when looking up a handle, not when looking up a DID. +var ErrHandleResolutionFailed = errors.New("handle resolution failed") + +// Indicates that resolution process completed successfully, but handle does not exist. This is only returned when looking up a handle, not when looking up a DID. +var ErrHandleNotFound = errors.New("handle not found") + +// Indicates that resolution process completed successfully, handle mapped to a different DID. This is only returned when looking up a handle, not when looking up a DID. +var ErrHandleMismatch = errors.New("handle/DID mismatch") + +// Indicates that DID document did not include any handle ("alsoKnownAs"). This is only returned when looking up a handle, not when looking up a DID. +var ErrHandleNotDeclared = errors.New("DID document did not declare a handle") + +// Handle top-level domain (TLD) is one of the special "Reserved" suffixes, and not allowed for atproto use +var ErrHandleReservedTLD = errors.New("handle top-level domain is disallowed") + +// Indicates that resolution process completed successfully, but the DID does not exist. +var ErrDIDNotFound = errors.New("DID not found") + +// Indicates that DID resolution process failed. A wrapped error may provide more context. +var ErrDIDResolutionFailed = errors.New("DID resolution failed") + +// Indicates that DID document did not include a public key with the specified ID +var ErrKeyNotDeclared = errors.New("DID document did not declare a relevant public key") + +var DefaultPLCURL = "https://plc.directory" + +// Returns a reasonable Directory implementation for applications +func DefaultDirectory() Directory { + base := BaseDirectory{ + PLCURL: DefaultPLCURL, + HTTPClient: http.Client{ + Timeout: time.Second * 10, + Transport: &http.Transport{ + // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad. + IdleConnTimeout: time.Millisecond * 1000, + MaxIdleConns: 100, + }, + }, + Resolver: net.Resolver{ + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{Timeout: time.Second * 3} + return d.DialContext(ctx, network, address) + }, + }, + TryAuthoritativeDNS: true, + // primary Bluesky PDS instance only supports HTTP resolution method + SkipDNSDomainSuffixes: []string{".bsky.social"}, + } + cached := NewCacheDirectory(&base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) + return &cached +} diff --git a/atproto/identity/identity.go b/atproto/identity/identity.go index c0453b2af..9779ca0d9 100644 --- a/atproto/identity/identity.go +++ b/atproto/identity/identity.go @@ -1,14 +1,9 @@ package identity import ( - "context" - "errors" "fmt" - "net" - "net/http" "net/url" "strings" - "time" "github.com/bluesky-social/indigo/atproto/crypto" "github.com/bluesky-social/indigo/atproto/syntax" @@ -16,76 +11,6 @@ import ( "github.com/mr-tron/base58" ) -// 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 -// -// 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. -// -// Some example implementations of this interface could be: -// - basic direct resolution on every call -// - local in-memory caching layer to reduce network hits -// - API client, which just makes requests to PDS (or other remote service) -// - client for shared network cache (eg, Redis) -type Directory interface { - LookupHandle(ctx context.Context, h syntax.Handle) (*Identity, error) - LookupDID(ctx context.Context, d syntax.DID) (*Identity, error) - Lookup(ctx context.Context, i syntax.AtIdentifier) (*Identity, error) - - // Flushes any cache of the indicated identifier. If directory is not using caching, can ignore this. - Purge(ctx context.Context, i syntax.AtIdentifier) error -} - -// Indicates that handle resolution failed. A wrapped error may provide more context. This is only returned when looking up a handle, not when looking up a DID. -var ErrHandleResolutionFailed = errors.New("handle resolution failed") - -// Indicates that resolution process completed successfully, but handle does not exist. This is only returned when looking up a handle, not when looking up a DID. -var ErrHandleNotFound = errors.New("handle not found") - -// Indicates that resolution process completed successfully, handle mapped to a different DID. This is only returned when looking up a handle, not when looking up a DID. -var ErrHandleMismatch = errors.New("handle/DID mismatch") - -// Indicates that DID document did not include any handle ("alsoKnownAs"). This is only returned when looking up a handle, not when looking up a DID. -var ErrHandleNotDeclared = errors.New("DID document did not declare a handle") - -// Handle top-level domain (TLD) is one of the special "Reserved" suffixes, and not allowed for atproto use -var ErrHandleReservedTLD = errors.New("handle top-level domain is disallowed") - -// Indicates that resolution process completed successfully, but the DID does not exist. -var ErrDIDNotFound = errors.New("DID not found") - -// Indicates that DID resolution process failed. A wrapped error may provide more context. -var ErrDIDResolutionFailed = errors.New("DID resolution failed") - -// Indicates that DID document did not include a public key with the specified ID -var ErrKeyNotDeclared = errors.New("DID document did not declare a relevant public key") - -var DefaultPLCURL = "https://plc.directory" - -// Returns a reasonable Directory implementation for applications -func DefaultDirectory() Directory { - base := BaseDirectory{ - PLCURL: DefaultPLCURL, - HTTPClient: http.Client{ - Timeout: time.Second * 10, - Transport: &http.Transport{ - // would want this around 100ms for services doing lots of handle resolution. Impacts PLC connections as well, but not too bad. - IdleConnTimeout: time.Millisecond * 1000, - MaxIdleConns: 100, - }, - }, - Resolver: net.Resolver{ - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{Timeout: time.Second * 3} - return d.DialContext(ctx, network, address) - }, - }, - TryAuthoritativeDNS: true, - // primary Bluesky PDS instance only supports HTTP resolution method - SkipDNSDomainSuffixes: []string{".bsky.social"}, - } - cached := NewCacheDirectory(&base, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) - return &cached -} - // Represents an atproto identity. Could be a regular user account, or a service account (eg, feed generator) type Identity struct { DID syntax.DID