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 -131
Interdiff #0 #1
-131
idresolver/resolver.go
··· 2 2 3 3 import ( 4 4 "context" 5 - "encoding/json" 6 5 "fmt" 7 6 "net" 8 7 "net/http" 9 - "strings" 10 8 "sync" 11 9 "time" 12 10 ··· 78 76 }, nil 79 77 } 80 78 81 - // DevDirectory wraps a BaseDirectory and resolves .test TLD handles via a 82 - // PDS's com.atproto.identity.resolveHandle endpoint instead of the standard 83 - // DNS TXT / HTTP well-known methods which cannot work for non-routable TLDs. 84 - // 85 - // For all other handles and DIDs, it delegates to the underlying BaseDirectory. 86 - // This allows local development with real handle resolution (no handle.invalid) 87 - // while keeping normal resolution for production handles. 88 - type DevDirectory struct { 89 - inner *identity.BaseDirectory 90 - pdsURL string 91 - } 92 - 93 - var _ identity.Directory = (*DevDirectory)(nil) 94 - 95 - func (d *DevDirectory) LookupHandle(ctx context.Context, h syntax.Handle) (*identity.Identity, error) { 96 - h = h.Normalize() 97 - did, err := d.resolveHandle(ctx, h) 98 - if err != nil { 99 - return nil, err 100 - } 101 - doc, err := d.inner.ResolveDID(ctx, did) 102 - if err != nil { 103 - return nil, err 104 - } 105 - ident := identity.ParseIdentity(doc) 106 - declared, err := ident.DeclaredHandle() 107 - if err != nil { 108 - return nil, fmt.Errorf("could not verify handle/DID match: %w", err) 109 - } 110 - if declared != h { 111 - return nil, fmt.Errorf("handle mismatch: %s != %s", declared, h) 112 - } 113 - ident.Handle = declared 114 - return &ident, nil 115 - } 116 - 117 - func (d *DevDirectory) LookupDID(ctx context.Context, did syntax.DID) (*identity.Identity, error) { 118 - doc, err := d.inner.ResolveDID(ctx, did) 119 - if err != nil { 120 - return nil, err 121 - } 122 - ident := identity.ParseIdentity(doc) 123 - declared, err := ident.DeclaredHandle() 124 - if err != nil { 125 - ident.Handle = syntax.HandleInvalid 126 - return &ident, nil 127 - } 128 - // Verify the declared handle resolves back to this DID. 129 - resolvedDID, err := d.resolveHandle(ctx, declared) 130 - if err != nil { 131 - ident.Handle = syntax.HandleInvalid 132 - } else if resolvedDID != did { 133 - ident.Handle = syntax.HandleInvalid 134 - } else { 135 - ident.Handle = declared 136 - } 137 - return &ident, nil 138 - } 139 - 140 - func (d *DevDirectory) Lookup(ctx context.Context, a syntax.AtIdentifier) (*identity.Identity, error) { 141 - handle, err := a.AsHandle() 142 - if nil == err { 143 - return d.LookupHandle(ctx, handle) 144 - } 145 - did, err := a.AsDID() 146 - if nil == err { 147 - return d.LookupDID(ctx, did) 148 - } 149 - return nil, fmt.Errorf("at-identifier neither a Handle nor a DID") 150 - } 151 - 152 - func (d *DevDirectory) Purge(ctx context.Context, atid syntax.AtIdentifier) error { 153 - return nil 154 - } 155 - 156 - // resolveHandle resolves a handle to a DID. For .test TLD handles, it queries 157 - // the configured PDS's com.atproto.identity.resolveHandle endpoint. For all 158 - // other handles, it delegates to the standard BaseDirectory resolution. 159 - func (d *DevDirectory) resolveHandle(ctx context.Context, h syntax.Handle) (syntax.DID, error) { 160 - if strings.HasSuffix(h.String(), ".test") { 161 - return d.resolveHandleViaPDS(ctx, h) 162 - } 163 - return d.inner.ResolveHandle(ctx, h) 164 - } 165 - 166 - // resolveHandleViaPDS calls com.atproto.identity.resolveHandle on the 167 - // configured PDS to resolve a handle that can't be resolved via DNS/HTTP. 168 - func (d *DevDirectory) resolveHandleViaPDS(ctx context.Context, h syntax.Handle) (syntax.DID, error) { 169 - url := fmt.Sprintf("%s/xrpc/com.atproto.identity.resolveHandle?handle=%s", d.pdsURL, h) 170 - req, err := http.NewRequestWithContext(ctx, "GET", url, nil) 171 - if err != nil { 172 - return "", fmt.Errorf("constructing PDS resolve request: %w", err) 173 - } 174 - 175 - resp, err := d.inner.HTTPClient.Do(req) 176 - if err != nil { 177 - return "", fmt.Errorf("PDS handle resolution failed: %w", err) 178 - } 179 - defer resp.Body.Close() 180 - 181 - if resp.StatusCode != http.StatusOK { 182 - return "", fmt.Errorf("PDS handle resolution returned HTTP %d for %s", resp.StatusCode, h) 183 - } 184 - 185 - var body struct { 186 - DID string `json:"did"` 187 - } 188 - if err := json.NewDecoder(resp.Body).Decode(&body); err != nil { 189 - return "", fmt.Errorf("invalid PDS resolve response: %w", err) 190 - } 191 - did, err := syntax.ParseDID(body.DID) 192 - if err != nil { 193 - return "", fmt.Errorf("invalid DID in PDS resolve response: %w", err) 194 - } 195 - return did, nil 196 - } 197 - 198 - // DefaultDevResolver returns a resolver for local development that resolves 199 - // .test TLD handles via a PDS endpoint. This enables proper handle resolution 200 - // for non-routable TLDs without skipping verification entirely. 201 - func DefaultDevResolver(plcUrl, pdsUrl string) *Resolver { 202 - base := BaseDirectory(plcUrl).(*identity.BaseDirectory) 203 - dir := &DevDirectory{inner: base, pdsURL: pdsUrl} 204 - cached := identity.NewCacheDirectory(dir, 250_000, time.Hour*24, time.Minute*2, time.Minute*5) 205 - return &Resolver{ 206 - directory: &cached, 207 - } 208 - } 209 - 210 79 func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) { 211 80 id, err := syntax.ParseAtIdentifier(arg) 212 81 if err != nil {
+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