Monorepo for Tangled
at local-dev 142 lines 4.5 kB view raw
1package idresolver 2 3import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 "strings" 9 "time" 10 11 "github.com/bluesky-social/indigo/atproto/identity" 12 "github.com/bluesky-social/indigo/atproto/syntax" 13) 14 15// DevDirectory wraps a BaseDirectory and resolves .test TLD handles via a 16// PDS's com.atproto.identity.resolveHandle endpoint instead of the standard 17// DNS TXT / HTTP well-known methods which cannot work for non-routable TLDs. 18// 19// For all other handles and DIDs, it delegates to the underlying BaseDirectory. 20// This allows local development with real handle resolution (no handle.invalid) 21// while keeping normal resolution for production handles. 22type DevDirectory struct { 23 inner *identity.BaseDirectory 24 pdsURL string 25} 26 27var _ identity.Directory = (*DevDirectory)(nil) 28 29func (d *DevDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*identity.Identity, error) { 30 h = h.Normalize() 31 did, err := d.resolveHandle(ctx, h) 32 if err != nil { 33 return nil, err 34 } 35 doc, err := d.inner.ResolveDID(ctx, did) 36 if err != nil { 37 return nil, err 38 } 39 ident := identity.ParseIdentity(doc) 40 declared, err := ident.DeclaredHandle() 41 if err != nil { 42 return nil, fmt.Errorf("could not verify handle/DID match: %w", err) 43 } 44 if declared != h { 45 return nil, fmt.Errorf("handle mismatch: %s != %s", declared, h) 46 } 47 ident.Handle = declared 48 return &ident, nil 49} 50 51func (d *DevDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 52 doc, err := d.inner.ResolveDID(ctx, did) 53 if err != nil { 54 return nil, err 55 } 56 ident := identity.ParseIdentity(doc) 57 declared, err := ident.DeclaredHandle() 58 if err != nil { 59 ident.Handle = syntax.HandleInvalid 60 return &ident, nil 61 } 62 // Verify the declared handle resolves back to this DID. 63 resolvedDID, err := d.resolveHandle(ctx, declared) 64 if err != nil { 65 ident.Handle = syntax.HandleInvalid 66 } else if resolvedDID != did { 67 ident.Handle = syntax.HandleInvalid 68 } else { 69 ident.Handle = declared 70 } 71 return &ident, nil 72} 73 74func (d *DevDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*identity.Identity, error) { 75 handle, err := a.AsHandle() 76 if nil == err { 77 return d.LookupHandle(ctx, handle) 78 } 79 did, err := a.AsDID() 80 if nil == err { 81 return d.LookupDID(ctx, did) 82 } 83 return nil, fmt.Errorf("at-identifier neither a Handle nor a DID") 84} 85 86func (d *DevDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { 87 return nil 88} 89 90// resolveHandle resolves a handle to a DID. For .test TLD handles, it queries 91// the configured PDS's com.atproto.identity.resolveHandle endpoint. For all 92// other handles, it delegates to the standard BaseDirectory resolution. 93func (d *DevDirectory) resolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { 94 if strings.HasSuffix(h.String(), ".test") { 95 return d.resolveHandleViaPDS(ctx, h) 96 } 97 return d.inner.ResolveHandle(ctx, h) 98} 99 100// resolveHandleViaPDS calls com.atproto.identity.resolveHandle on the 101// configured PDS to resolve a handle that can't be resolved via DNS/HTTP. 102func (d *DevDirectory) resolveHandleViaPDS(ctx context.Context, h syntax.Handle) (syntax.DID, error) { 103 url := fmt.Sprintf("%s/xrpc/com.atproto.identity.resolveHandle?handle=%s", d.pdsURL, h) 104 req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 105 if err != nil { 106 return "", fmt.Errorf("constructing PDS resolve request: %w", err) 107 } 108 109 resp, err := d.inner.HTTPClient.Do(req) 110 if err != nil { 111 return "", fmt.Errorf("PDS handle resolution failed: %w", err) 112 } 113 defer resp.Body.Close() 114 115 if resp.StatusCode != http.StatusOK { 116 return "", fmt.Errorf("PDS handle resolution returned HTTP %d for %s", resp.StatusCode, h) 117 } 118 119 var body struct { 120 DID string `json:"did"` 121 } 122 if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { 123 return "", fmt.Errorf("invalid PDS resolve response: %w", err) 124 } 125 did, err := syntax.ParseDID(body.DID) 126 if err != nil { 127 return "", fmt.Errorf("invalid DID in PDS resolve response: %w", err) 128 } 129 return did, nil 130} 131 132// DefaultDevResolver returns a resolver for local development that resolves 133// .test TLD handles via a PDS endpoint. This enables proper handle resolution 134// for non-routable TLDs without skipping verification entirely. 135func DefaultDevResolver(plcUrl, pdsUrl string) *Resolver { 136 base := BaseDirectory(plcUrl).(*identity.BaseDirectory) 137 dir := &DevDirectory{inner: base, pdsURL: pdsUrl} 138 cached := identity.NewCacheDirectory(dir, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) 139 return &Resolver{ 140 directory: &cached, 141 } 142}