Monorepo for Tangled

idresolver: add DevDirectory for .test TLD handle resolution #2

open opened by nolith.dev targeting master from local-dev

Add DevDirectory type that wraps BaseDirectory and intercepts .test TLD handles, resolving them via the PDS's com.atproto.identity.resolveHandle endpoint instead of the standard DNS TXT / HTTP well-known methods which cannot work for non-routable TLDs.

For all other handles and DIDs, it delegates to the underlying BaseDirectory. This enables local development with real handle resolution (no handle.invalid) while keeping normal resolution for production handles.

Add DefaultDevResolver() constructor that creates a Resolver with a DevDirectory wrapping a cached BaseDirectory.

AI-assisted: GitLab Duo Agentic Chat (Claude Opus 4.6) Signed-off-by: Alessio Caiazza code.git@caiazza.info

Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:nzep3slobztdph3kxswzbing/sh.tangled.repo.pull/3mgmn4nau2f22
+142
Diff #1
+142
idresolver/dev_directory.go
··· 1 + package idresolver 2 + 3 + import ( 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. 22 + type DevDirectory struct { 23 + inner *identity.BaseDirectory 24 + pdsURL string 25 + } 26 + 27 + var _ identity.Directory = (*DevDirectory)(nil) 28 + 29 + func (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 + 51 + func (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 + 74 + func (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 + 86 + func (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. 93 + func (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. 102 + func (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. 135 + func 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 + }

History

2 rounds 0 comments
sign up or login to add to the discussion
1 commit
expand
idresolver: add DevDirectory for .test TLD handle resolution
no conflicts, ready to merge
expand 0 comments
1 commit
expand
idresolver: add DevDirectory for .test TLD handle resolution
expand 0 comments