A community based topic aggregation platform built on atproto
1package communities 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 9 "github.com/bluesky-social/indigo/api/atproto" 10 "github.com/bluesky-social/indigo/xrpc" 11) 12 13// refreshPDSToken exchanges a refresh token for new access and refresh tokens 14// Uses com.atproto.server.refreshSession endpoint via Indigo SDK 15// CRITICAL: Refresh tokens are single-use - old refresh token is revoked on success 16func refreshPDSToken(ctx context.Context, pdsURL, currentAccessToken, refreshToken string) (newAccessToken, newRefreshToken string, err error) { 17 if pdsURL == "" { 18 return "", "", fmt.Errorf("PDS URL is required") 19 } 20 if refreshToken == "" { 21 return "", "", fmt.Errorf("refresh token is required") 22 } 23 24 // Create XRPC client with auth credentials 25 // The refresh endpoint requires authentication with the refresh token 26 client := &xrpc.Client{ 27 Host: pdsURL, 28 Auth: &xrpc.AuthInfo{ 29 AccessJwt: currentAccessToken, // Can be expired (not used for refresh auth) 30 RefreshJwt: refreshToken, // This is what authenticates the refresh request 31 }, 32 } 33 34 // Call com.atproto.server.refreshSession 35 output, err := atproto.ServerRefreshSession(ctx, client) 36 if err != nil { 37 // Check for expired refresh token (401 Unauthorized) 38 // Try typed error first (more reliable), fallback to string check 39 var xrpcErr *xrpc.Error 40 if errors.As(err, &xrpcErr) && xrpcErr.StatusCode == 401 { 41 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)") 42 } 43 44 // Fallback: string-based detection (in case error isn't wrapped as xrpc.Error) 45 errStr := err.Error() 46 if strings.Contains(errStr, "401") || strings.Contains(errStr, "Unauthorized") { 47 return "", "", fmt.Errorf("refresh token expired or invalid (needs password re-auth)") 48 } 49 50 return "", "", fmt.Errorf("failed to refresh session: %w", err) 51 } 52 53 // Validate response 54 if output.AccessJwt == "" || output.RefreshJwt == "" { 55 return "", "", fmt.Errorf("refresh response missing tokens") 56 } 57 58 return output.AccessJwt, output.RefreshJwt, nil 59} 60 61// reauthenticateWithPassword creates a new session using stored credentials 62// This is the fallback when refresh tokens expire (after ~2 months) 63// Uses com.atproto.server.createSession endpoint via Indigo SDK 64func reauthenticateWithPassword(ctx context.Context, pdsURL, email, password string) (accessToken, refreshToken string, err error) { 65 if pdsURL == "" { 66 return "", "", fmt.Errorf("PDS URL is required") 67 } 68 if email == "" { 69 return "", "", fmt.Errorf("email is required") 70 } 71 if password == "" { 72 return "", "", fmt.Errorf("password is required") 73 } 74 75 // Create unauthenticated XRPC client 76 client := &xrpc.Client{ 77 Host: pdsURL, 78 } 79 80 // Prepare createSession input 81 // The identifier can be either email or handle 82 input := &atproto.ServerCreateSession_Input{ 83 Identifier: email, 84 Password: password, 85 } 86 87 // Call com.atproto.server.createSession 88 output, err := atproto.ServerCreateSession(ctx, client, input) 89 if err != nil { 90 return "", "", fmt.Errorf("failed to create session: %w", err) 91 } 92 93 // Validate response 94 if output.AccessJwt == "" || output.RefreshJwt == "" { 95 return "", "", fmt.Errorf("createSession response missing tokens") 96 } 97 98 return output.AccessJwt, output.RefreshJwt, nil 99}