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
+131
Diff #0
+131
idresolver/resolver.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 5 6 "fmt" 6 7 "net" 7 8 "net/http" 9 + "strings" 8 10 "sync" 9 11 "time" 10 12 ··· 76 78 }, nil 77 79 } 78 80 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 + 79 210 func (r *Resolver) ResolveIdent(ctx context.Context, arg string) (*identity.Identity, error) { 80 211 id, err := syntax.ParseAtIdentifier(arg) 81 212 if err != nil {

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
nolith.dev submitted #0
1 commit
expand
idresolver: add DevDirectory for .test TLD handle resolution
expand 0 comments