A community based topic aggregation platform built on atproto
1//go:build dev
2
3package oauth
4
5import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "log/slog"
10 "net/http"
11 "net/url"
12 "strings"
13 "time"
14)
15
16// DevHandleResolver resolves handles via local PDS for development
17// This is needed because local handles (e.g., user.local.coves.dev) can't be
18// resolved via standard DNS/HTTP well-known methods - they only exist on the local PDS.
19type DevHandleResolver struct {
20 pdsURL string
21 httpClient *http.Client
22}
23
24// NewDevHandleResolver creates a resolver that queries local PDS for handle resolution
25func NewDevHandleResolver(pdsURL string, allowPrivateIPs bool) *DevHandleResolver {
26 return &DevHandleResolver{
27 pdsURL: strings.TrimSuffix(pdsURL, "/"),
28 httpClient: NewSSRFSafeHTTPClient(allowPrivateIPs),
29 }
30}
31
32// ResolveHandle queries the local PDS to resolve a handle to a DID
33// Returns the DID if successful, or empty string if not found
34func (r *DevHandleResolver) ResolveHandle(ctx context.Context, handle string) (string, error) {
35 if r.pdsURL == "" {
36 return "", fmt.Errorf("PDS URL not configured")
37 }
38
39 // Build the resolve handle URL
40 resolveURL := fmt.Sprintf("%s/xrpc/com.atproto.identity.resolveHandle?handle=%s",
41 r.pdsURL, url.QueryEscape(handle))
42
43 // Create request with context and timeout
44 ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
45 defer cancel()
46
47 req, err := http.NewRequestWithContext(ctx, "GET", resolveURL, nil)
48 if err != nil {
49 return "", fmt.Errorf("failed to create request: %w", err)
50 }
51 req.Header.Set("User-Agent", "Coves/1.0")
52
53 // Execute request
54 resp, err := r.httpClient.Do(req)
55 if err != nil {
56 return "", fmt.Errorf("failed to query PDS: %w", err)
57 }
58 defer resp.Body.Close()
59
60 // Check response status
61 if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest {
62 return "", nil // Handle not found
63 }
64 if resp.StatusCode != http.StatusOK {
65 return "", fmt.Errorf("PDS returned status %d", resp.StatusCode)
66 }
67
68 // Parse response
69 var result struct {
70 DID string `json:"did"`
71 }
72 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
73 return "", fmt.Errorf("failed to parse PDS response: %w", err)
74 }
75
76 if result.DID == "" {
77 return "", nil // No DID in response
78 }
79
80 slog.Debug("resolved handle via local PDS",
81 "handle", handle,
82 "did", result.DID,
83 "pds_url", r.pdsURL)
84
85 return result.DID, nil
86}
87
88// ResolveIdentifier attempts to resolve a handle to DID, or returns the DID if already provided
89// This is the main entry point for the handlers
90func (r *DevHandleResolver) ResolveIdentifier(ctx context.Context, identifier string) (string, error) {
91 // If it's already a DID, return as-is
92 if strings.HasPrefix(identifier, "did:") {
93 return identifier, nil
94 }
95
96 // Try to resolve the handle via local PDS
97 did, err := r.ResolveHandle(ctx, identifier)
98 if err != nil {
99 return "", fmt.Errorf("failed to resolve handle via PDS: %w", err)
100 }
101 if did == "" {
102 return "", fmt.Errorf("handle not found on local PDS: %s", identifier)
103 }
104
105 return did, nil
106}