A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at label-service 173 lines 4.9 kB view raw
1package main 2 3import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "io" 8 "net/http" 9 "os" 10 "time" 11) 12 13// Device authorization API types 14 15type DeviceCodeRequest struct { 16 DeviceName string `json:"device_name"` 17} 18 19type DeviceCodeResponse struct { 20 DeviceCode string `json:"device_code"` 21 UserCode string `json:"user_code"` 22 VerificationURI string `json:"verification_uri"` 23 ExpiresIn int `json:"expires_in"` 24 Interval int `json:"interval"` 25} 26 27type DeviceTokenRequest struct { 28 DeviceCode string `json:"device_code"` 29} 30 31type DeviceTokenResponse struct { 32 DeviceSecret string `json:"device_secret,omitempty"` 33 Handle string `json:"handle,omitempty"` 34 DID string `json:"did,omitempty"` 35 Error string `json:"error,omitempty"` 36} 37 38// AuthErrorResponse is the JSON error response from /auth/token 39type AuthErrorResponse struct { 40 Error string `json:"error"` 41 Message string `json:"message"` 42 LoginURL string `json:"login_url,omitempty"` 43} 44 45// ValidationResult represents the result of credential validation 46type ValidationResult struct { 47 Valid bool 48 OAuthSessionExpired bool 49 LoginURL string 50} 51 52// requestDeviceCode requests a device code from the AppView. 53// Returns the code response and resolved AppView URL. 54// Does not print anything — the caller controls UX. 55func requestDeviceCode(serverURL string) (*DeviceCodeResponse, string, error) { 56 appViewURL := buildAppViewURL(serverURL) 57 deviceName := hostname() 58 59 reqBody, _ := json.Marshal(DeviceCodeRequest{DeviceName: deviceName}) 60 resp, err := http.Post(appViewURL+"/auth/device/code", "application/json", bytes.NewReader(reqBody)) 61 if err != nil { 62 return nil, appViewURL, fmt.Errorf("failed to request device code: %w", err) 63 } 64 defer resp.Body.Close() 65 66 if resp.StatusCode != http.StatusOK { 67 body, _ := io.ReadAll(resp.Body) 68 return nil, appViewURL, fmt.Errorf("device code request failed: %s", string(body)) 69 } 70 71 var codeResp DeviceCodeResponse 72 if err := json.NewDecoder(resp.Body).Decode(&codeResp); err != nil { 73 return nil, appViewURL, fmt.Errorf("failed to decode device code response: %w", err) 74 } 75 76 return &codeResp, appViewURL, nil 77} 78 79// pollDeviceToken polls the token endpoint until authorization completes. 80// Does not print anything — the caller controls UX. 81// Returns the account on success, or an error on timeout/failure. 82func pollDeviceToken(appViewURL string, codeResp *DeviceCodeResponse) (*Account, error) { 83 pollInterval := time.Duration(codeResp.Interval) * time.Second 84 timeout := time.Duration(codeResp.ExpiresIn) * time.Second 85 deadline := time.Now().Add(timeout) 86 87 for time.Now().Before(deadline) { 88 time.Sleep(pollInterval) 89 90 tokenReqBody, _ := json.Marshal(DeviceTokenRequest{DeviceCode: codeResp.DeviceCode}) 91 tokenResp, err := http.Post(appViewURL+"/auth/device/token", "application/json", bytes.NewReader(tokenReqBody)) 92 if err != nil { 93 continue 94 } 95 96 var tokenResult DeviceTokenResponse 97 if err := json.NewDecoder(tokenResp.Body).Decode(&tokenResult); err != nil { 98 tokenResp.Body.Close() 99 continue 100 } 101 tokenResp.Body.Close() 102 103 if tokenResult.Error == "authorization_pending" { 104 continue 105 } 106 107 if tokenResult.Error != "" { 108 return nil, fmt.Errorf("authorization failed: %s", tokenResult.Error) 109 } 110 111 return &Account{ 112 Handle: tokenResult.Handle, 113 DID: tokenResult.DID, 114 DeviceSecret: tokenResult.DeviceSecret, 115 }, nil 116 } 117 118 return nil, fmt.Errorf("authorization timed out") 119} 120 121// validateCredentials checks if the credentials are still valid by making a test request 122func validateCredentials(appViewURL, handle, deviceSecret string) ValidationResult { 123 client := &http.Client{ 124 Timeout: 5 * time.Second, 125 } 126 127 tokenURL := appViewURL + "/auth/token?service=" + appViewURL 128 129 req, err := http.NewRequest("GET", tokenURL, nil) 130 if err != nil { 131 return ValidationResult{Valid: false} 132 } 133 134 req.SetBasicAuth(handle, deviceSecret) 135 136 resp, err := client.Do(req) 137 if err != nil { 138 // Network error — assume credentials are valid but server unreachable 139 return ValidationResult{Valid: true} 140 } 141 defer resp.Body.Close() 142 143 if resp.StatusCode == http.StatusOK { 144 return ValidationResult{Valid: true} 145 } 146 147 if resp.StatusCode == http.StatusUnauthorized { 148 body, err := io.ReadAll(resp.Body) 149 if err == nil { 150 var authErr AuthErrorResponse 151 if json.Unmarshal(body, &authErr) == nil && authErr.Error == "oauth_session_expired" { 152 return ValidationResult{ 153 Valid: false, 154 OAuthSessionExpired: true, 155 LoginURL: authErr.LoginURL, 156 } 157 } 158 } 159 return ValidationResult{Valid: false} 160 } 161 162 // Any other error = assume valid (don't re-auth on server issues) 163 return ValidationResult{Valid: true} 164} 165 166// hostname returns the machine hostname, or a fallback. 167func hostname() string { 168 name, err := os.Hostname() 169 if err != nil { 170 return "Unknown Device" 171 } 172 return name 173}