[DEPRECATED] Go implementation of plcbundle
at rust-test 130 lines 3.4 kB view raw
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}