forked from
tangled.org/core
Monorepo for Tangled
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}