[DEPRECATED] Go implementation of plcbundle
1package handleresolver
2
3import (
4 "context"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "regexp"
10 "strings"
11 "time"
12
13 "github.com/goccy/go-json"
14)
15
16// Client resolves AT Protocol handles to DIDs via XRPC
17type Client struct {
18 baseURL string
19 httpClient *http.Client
20}
21
22// NewClient creates a new handle resolver client
23func NewClient(baseURL string) *Client {
24 return &Client{
25 baseURL: strings.TrimSuffix(baseURL, "/"),
26 httpClient: &http.Client{
27 Timeout: 10 * time.Second,
28 },
29 }
30}
31
32// ResolveHandle resolves a handle to a DID using com.atproto.identity.resolveHandle
33func (c *Client) ResolveHandle(ctx context.Context, handle string) (string, error) {
34 // Validate handle format
35 if err := ValidateHandleFormat(handle); err != nil {
36 return "", err
37 }
38
39 // Build XRPC URL
40 endpoint := fmt.Sprintf("%s/xrpc/com.atproto.identity.resolveHandle", c.baseURL)
41
42 // Add query parameter
43 params := url.Values{}
44 params.Add("handle", handle)
45 fullURL := fmt.Sprintf("%s?%s", endpoint, params.Encode())
46
47 // Create request
48 req, err := http.NewRequestWithContext(ctx, "GET", fullURL, nil)
49 if err != nil {
50 return "", fmt.Errorf("failed to create request: %w", err)
51 }
52
53 // Execute request
54 resp, err := c.httpClient.Do(req)
55 if err != nil {
56 return "", fmt.Errorf("failed to resolve handle: %w", err)
57 }
58 defer resp.Body.Close()
59
60 if resp.StatusCode != http.StatusOK {
61 body, _ := io.ReadAll(resp.Body)
62 return "", fmt.Errorf("resolver returned status %d: %s", resp.StatusCode, string(body))
63 }
64
65 // Parse response
66 var result struct {
67 DID string `json:"did"`
68 }
69 if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
70 return "", fmt.Errorf("failed to parse response: %w", err)
71 }
72
73 if result.DID == "" {
74 return "", fmt.Errorf("resolver returned empty DID")
75 }
76
77 // Validate returned DID
78 if !strings.HasPrefix(result.DID, "did:plc:") && !strings.HasPrefix(result.DID, "did:web:") {
79 return "", fmt.Errorf("invalid DID format returned: %s", result.DID)
80 }
81
82 return result.DID, nil
83}
84
85// ValidateHandleFormat validates AT Protocol handle format
86func ValidateHandleFormat(handle string) error {
87 if handle == "" {
88 return fmt.Errorf("handle cannot be empty")
89 }
90
91 // Handle can't be a DID
92 if strings.HasPrefix(handle, "did:") {
93 return fmt.Errorf("input is already a DID, not a handle")
94 }
95
96 // Basic length check
97 if len(handle) > 253 {
98 return fmt.Errorf("handle too long (max 253 chars)")
99 }
100
101 // Must have at least one dot (domain.tld)
102 if !strings.Contains(handle, ".") {
103 return fmt.Errorf("handle must be a domain (e.g., user.bsky.social)")
104 }
105
106 // Valid handle pattern (simplified - matches AT Protocol spec)
107 validPattern := regexp.MustCompile(`^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$`)
108 if !validPattern.MatchString(handle) {
109 return fmt.Errorf("invalid handle format")
110 }
111
112 return nil
113}
114
115// IsHandle checks if a string looks like a handle (not a DID)
116func IsHandle(input string) bool {
117 return !strings.HasPrefix(input, "did:")
118}
119
120// NormalizeHandle normalizes handle format (removes at:// prefix if present)
121func NormalizeHandle(handle string) string {
122 handle = strings.TrimPrefix(handle, "at://")
123 handle = strings.TrimPrefix(handle, "@")
124 return handle
125}
126
127// GetBaseURL returns the PLC directory base URL
128func (c *Client) GetBaseURL() string {
129 return c.baseURL
130}