├── .gitignore ├── Makefile ├── .goreleaser.yml ├── go.mod ├── LICENSE ├── pkg └── plaid_cli │ ├── plaid_cli.go │ └── linker.go ├── README.md ├── main.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .envrc 2 | bin 3 | dist 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o bin/plaid-cli 3 | 4 | release: 5 | goreleaser --rm-dist 6 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | # You may remove this if you don't use go modules. 6 | - go mod download 7 | # you may remove this if you don't need go generate 8 | - go generate ./... 9 | builds: 10 | - env: 11 | - CGO_ENABLED=0 12 | ignore: 13 | - goos: darwin 14 | goarch: 386 15 | archives: 16 | - replacements: 17 | darwin: Darwin 18 | linux: Linux 19 | windows: Windows 20 | 386: i386 21 | amd64: x86_64 22 | checksum: 23 | name_template: 'checksums.txt' 24 | snapshot: 25 | name_template: "{{ .Tag }}-next" 26 | changelog: 27 | sort: asc 28 | filters: 29 | exclude: 30 | - '^docs:' 31 | - '^test:' 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/landakram/plaid-cli 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/Xuanwo/go-locale v1.0.0 7 | github.com/fsnotify/fsnotify v1.4.9 // indirect 8 | github.com/manifoldco/promptui v0.7.0 9 | github.com/mitchellh/mapstructure v1.3.2 // indirect 10 | github.com/pelletier/go-toml v1.8.0 // indirect 11 | github.com/plaid/plaid-go v0.0.0-20210112002311-0cf0e6f0ea3e 12 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 13 | github.com/spf13/afero v1.2.2 // indirect 14 | github.com/spf13/cast v1.3.1 // indirect 15 | github.com/spf13/cobra v1.0.0 16 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 17 | github.com/spf13/pflag v1.0.5 // indirect 18 | github.com/spf13/viper v1.7.0 19 | github.com/stretchr/testify v1.6.1 // indirect 20 | golang.org/x/text v0.3.3 21 | gopkg.in/ini.v1 v1.57.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Mark Hudnall 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/plaid_cli/plaid_cli.go: -------------------------------------------------------------------------------- 1 | package plaid_cli 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | ) 10 | 11 | type Data struct { 12 | DataDir string 13 | Tokens map[string]string 14 | Aliases map[string]string 15 | BackAliases map[string]string 16 | } 17 | 18 | func LoadData(dataDir string) (*Data, error) { 19 | os.MkdirAll(filepath.Join(dataDir, "data"), os.ModePerm) 20 | 21 | data := &Data{ 22 | DataDir: dataDir, 23 | BackAliases: make(map[string]string), 24 | } 25 | 26 | data.loadTokens() 27 | data.loadAliases() 28 | 29 | return data, nil 30 | } 31 | 32 | func (d *Data) loadAliases() { 33 | var aliases map[string]string = make(map[string]string) 34 | filePath := d.aliasesPath() 35 | err := load(filePath, &aliases) 36 | if err != nil { 37 | log.Printf("Error loading aliases from %s. Assuming empty tokens. Error: %s", d.aliasesPath(), err) 38 | } 39 | 40 | d.Aliases = aliases 41 | 42 | for alias, itemID := range aliases { 43 | d.BackAliases[itemID] = alias 44 | } 45 | } 46 | 47 | func (d *Data) tokensPath() string { 48 | return filepath.Join(d.DataDir, "data", "tokens.json") 49 | } 50 | 51 | func (d *Data) aliasesPath() string { 52 | return filepath.Join(d.DataDir, "data", "aliases.json") 53 | } 54 | 55 | func (d *Data) loadTokens() { 56 | var tokens map[string]string = make(map[string]string) 57 | filePath := d.tokensPath() 58 | err := load(filePath, &tokens) 59 | if err != nil { 60 | log.Printf("Error loading tokens from %s. Assuming empty tokens. Error: %s", d.tokensPath(), err) 61 | } 62 | 63 | d.Tokens = tokens 64 | } 65 | 66 | func load(filePath string, v interface{}) error { 67 | f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) 68 | defer f.Close() 69 | 70 | if err != nil { 71 | return err 72 | } else { 73 | b, err := ioutil.ReadAll(f) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | return json.Unmarshal(b, v) 79 | } 80 | } 81 | 82 | func (d *Data) Save() error { 83 | err := d.SaveTokens() 84 | if err != nil { 85 | return err 86 | } 87 | 88 | err = d.SaveAliases() 89 | if err != nil { 90 | return err 91 | } 92 | 93 | return nil 94 | } 95 | 96 | func (d *Data) SaveTokens() error { 97 | return save(d.Tokens, d.tokensPath()) 98 | } 99 | 100 | func (d *Data) SaveAliases() error { 101 | return save(d.Aliases, d.aliasesPath()) 102 | } 103 | 104 | func save(v interface{}, filePath string) error { 105 | f, err := os.OpenFile(filePath, os.O_RDWR|os.O_CREATE, 0755) 106 | if err != nil { 107 | return err 108 | } 109 | defer f.Close() 110 | 111 | b, err := json.Marshal(v) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | _, err = f.Write(b) 117 | return err 118 | } 119 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # plaid-cli 🤑 2 | 3 | > Link accounts and get transactions from the command line. 4 | 5 | plaid-cli is a CLI tool for working with the Plaid API. 6 | 7 | You can use plaid-cli to link bank accounts and pull transactions in multiple 8 | output formats from the comfort of the command line. 9 | 10 | ## Installation 11 | 12 | Install with `go get`: 13 | 14 | ```sh 15 | go get github.com/landakram/plaid-cli 16 | ``` 17 | 18 | Or grab a binary for your platform from the [Releases](https://github.com/landakram/plaid-cli/releases) page. 19 | 20 | ## Configuration 21 | 22 | To get started, you'll need Plaid API credentials, which you can get by visiting 23 | https://dashboard.plaid.com/team/keys after signing up for free. 24 | 25 | plaid-cli will look at the following environment variables for API credentials: 26 | 27 | ```sh 28 | PLAID_CLIENT_ID= 29 | PLAID_SECRET= 30 | PLAID_ENVIRONMENT=development 31 | PLAID_LANGUAGE=en # optional, detected using system's locale 32 | PLAID_COUNTRIES=US # optional, detected using system's locale 33 | ``` 34 | 35 | I recommend setting and exporting these on shell startup. 36 | 37 | API credentials can also be specified using a config file located at 38 | ~/.plaid-cli/config.toml: 39 | 40 | ```toml 41 | [plaid] 42 | client_id = "" 43 | secret = "" 44 | environment = "development" 45 | ``` 46 | 47 | After setting those API credentials, plaid-cli is ready to use! 48 | You'll probably want to run 'plaid-cli link' next. 49 | 50 | ## Usage 51 | 52 |
 53 | Usage:
 54 |   plaid-cli [command]
 55 | 
 56 | Available Commands:
 57 |   accounts     List accounts for a given institution
 58 |   alias        Give a linked bank account a name.
 59 |   aliases      List aliases
 60 |   help         Help about any command
 61 |   link         Link a bank account so plaid-cli can pull transactions.
 62 |   tokens       List tokens
 63 |   transactions List transactions for a given account
 64 | 
 65 | Flags:
 66 |   -h, --help   help for plaid-cli
 67 | 
 68 | Use "plaid-cli [command] --help" for more information about a command.
 69 | 
70 | 71 | ### Link an account 72 | 73 | Run: 74 | 75 | ``` 76 | plaid-cli link 77 | ``` 78 | 79 | plaid-cli will start a webserver and open your browser so you can link your bank account 80 | with [Plaid Link](https://blog.plaid.com/plaid-link/). 81 | 82 | To see the access token you just created and the "Plaid Item ID" it's associated with, 83 | you can run: 84 | 85 | ``` 86 | plaid-cli tokens 87 | ``` 88 | 89 | ### Alias a link 90 | 91 | You can make human-readable names for a linked instituion by running: 92 | 93 | ``` 94 | plaid-cli alias nice-name 95 | ``` 96 | 97 | You can now refer to the linked instituion by `nice-name` in most commands. 98 | 99 | ### Pulling transactions 100 | 101 | You can pull transaction history for an institution by running: 102 | 103 | ``` 104 | plaid-cli transactions --from 2020-06-01 --to 2020-06-10 --output-format csv > out.csv 105 | ``` 106 | 107 | The output is suitable for manual import in budgeting tools such as YNAB. 108 | 109 | ### Relinking 110 | 111 | Most commands will prompt you to relink automatically if your bank login has expired (due to 2FA, for example). 112 | 113 | To manually relink, you can run the link command with an item ID or alias: 114 | 115 | ``` 116 | plaid-cli link nice-name 117 | ``` 118 | 119 | ## Why 120 | 121 | I wanted to work around YNAB's flaky direct import feature. For some reason, it's not able 122 | to sync transactions with SoFi and SoFi only provides a PDF statement history unsuitable for 123 | manual import. 124 | 125 | Similar projects: 126 | 127 | * [plaid2qif](https://github.com/ebridges/plaid2qif). A very similar Python-based cli tool. The major difference is that plaid-cli handles linking to account automatically and will prompt for relinks. 128 | -------------------------------------------------------------------------------- /pkg/plaid_cli/linker.go: -------------------------------------------------------------------------------- 1 | package plaid_cli 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | "text/template" 10 | 11 | "github.com/plaid/plaid-go/plaid" 12 | "github.com/skratchdot/open-golang/open" 13 | ) 14 | 15 | type Linker struct { 16 | Results chan string 17 | RelinkResults chan bool 18 | Errors chan error 19 | Client *plaid.Client 20 | Data *Data 21 | countries []string 22 | lang string 23 | } 24 | 25 | type TokenPair struct { 26 | ItemID string 27 | AccessToken string 28 | } 29 | 30 | func (l *Linker) Relink(itemID string, port string) error { 31 | token := l.Data.Tokens[itemID] 32 | hostname, err := os.Hostname() 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | resp, err := l.Client.CreateLinkToken(plaid.LinkTokenConfigs{ 37 | User: &plaid.LinkTokenUser{ 38 | ClientUserID: hostname, 39 | }, 40 | ClientName: "plaid-cli", 41 | CountryCodes: l.countries, 42 | Language: l.lang, 43 | AccessToken: token, 44 | }) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | return l.relink(port, resp.LinkToken) 49 | } 50 | 51 | func (l *Linker) Link(port string) (*TokenPair, error) { 52 | hostname, err := os.Hostname() 53 | if err != nil { 54 | log.Fatal(err) 55 | } 56 | resp, err := l.Client.CreateLinkToken(plaid.LinkTokenConfigs{ 57 | User: &plaid.LinkTokenUser{ 58 | ClientUserID: hostname, 59 | }, 60 | ClientName: "plaid-cli", 61 | Products: []string{"transactions"}, 62 | CountryCodes: l.countries, 63 | Language: l.lang, 64 | }) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | return l.link(port, resp.LinkToken) 69 | } 70 | 71 | func (l *Linker) link(port string, linkToken string) (*TokenPair, error) { 72 | log.Println(fmt.Sprintf("Starting Plaid Link on port %s...", port)) 73 | 74 | go func() { 75 | http.HandleFunc("/link", handleLink(l, linkToken)) 76 | err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 77 | if err != nil { 78 | l.Errors <- err 79 | } 80 | }() 81 | 82 | url := fmt.Sprintf("http://localhost:%s/link", port) 83 | log.Println(fmt.Sprintf("Your browser should open automatically. If it doesn't, please visit %s to continue linking!", url)) 84 | open.Run(url) 85 | 86 | select { 87 | case err := <-l.Errors: 88 | return nil, err 89 | case publicToken := <-l.Results: 90 | 91 | res, err := l.exchange(publicToken) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | pair := &TokenPair{ 97 | ItemID: res.ItemID, 98 | AccessToken: res.AccessToken, 99 | } 100 | 101 | return pair, nil 102 | } 103 | } 104 | 105 | func (l *Linker) relink(port string, linkToken string) error { 106 | log.Println(fmt.Sprintf("Starting Plaid Link on port %s...", port)) 107 | 108 | go func() { 109 | http.HandleFunc("/relink", handleRelink(l, linkToken)) 110 | err := http.ListenAndServe(fmt.Sprintf(":%s", port), nil) 111 | if err != nil { 112 | l.Errors <- err 113 | } 114 | }() 115 | 116 | url := fmt.Sprintf("http://localhost:%s/relink", port) 117 | log.Println(fmt.Sprintf("Your browser should open automatically. If it doesn't, please visit %s to continue linking!", url)) 118 | open.Run(url) 119 | 120 | select { 121 | case err := <-l.Errors: 122 | return err 123 | case <-l.RelinkResults: 124 | return nil 125 | } 126 | } 127 | 128 | func (l *Linker) exchange(publicToken string) (plaid.ExchangePublicTokenResponse, error) { 129 | return l.Client.ExchangePublicToken(publicToken) 130 | } 131 | 132 | func NewLinker(data *Data, client *plaid.Client, countries []string, lang string) *Linker { 133 | return &Linker{ 134 | Results: make(chan string), 135 | RelinkResults: make(chan bool), 136 | Errors: make(chan error), 137 | Client: client, 138 | Data: data, 139 | countries: countries, 140 | lang: lang, 141 | } 142 | } 143 | 144 | func handleLink(linker *Linker, linkToken string) func(w http.ResponseWriter, r *http.Request) { 145 | return func(w http.ResponseWriter, r *http.Request) { 146 | switch r.Method { 147 | case http.MethodGet: 148 | t := template.New("link") 149 | t, _ = t.Parse(linkTemplate) 150 | 151 | d := LinkTmplData{ 152 | LinkToken: linkToken, 153 | } 154 | t.Execute(w, d) 155 | case http.MethodPost: 156 | r.ParseForm() 157 | token := r.Form.Get("public_token") 158 | if token != "" { 159 | linker.Results <- token 160 | } else { 161 | linker.Errors <- errors.New("Empty public_token") 162 | } 163 | 164 | fmt.Fprintf(w, "ok") 165 | default: 166 | linker.Errors <- errors.New("Invalid HTTP method") 167 | } 168 | } 169 | } 170 | 171 | type LinkTmplData struct { 172 | LinkToken string 173 | } 174 | 175 | type RelinkTmplData struct { 176 | LinkToken string 177 | } 178 | 179 | func handleRelink(linker *Linker, linkToken string) func(w http.ResponseWriter, r *http.Request) { 180 | return func(w http.ResponseWriter, r *http.Request) { 181 | switch r.Method { 182 | case http.MethodGet: 183 | t := template.New("relink") 184 | t, _ = t.Parse(relinkTemplate) 185 | 186 | d := RelinkTmplData{ 187 | LinkToken: linkToken, 188 | } 189 | t.Execute(w, d) 190 | case http.MethodPost: 191 | r.ParseForm() 192 | err := r.Form.Get("error") 193 | if err != "" { 194 | linker.Errors <- errors.New(err) 195 | } else { 196 | linker.RelinkResults <- true 197 | } 198 | 199 | fmt.Fprintf(w, "ok") 200 | default: 201 | linker.Errors <- errors.New("Invalid HTTP method") 202 | } 203 | } 204 | } 205 | 206 | var linkTemplate string = ` 207 | 208 | 225 | 226 | 227 | 228 | 229 | 260 | 261 | 267 | 268 | ` 269 | 270 | var relinkTemplate string = ` 271 | 272 | 289 | 290 | 291 | 292 | 293 | 324 | 325 | 331 | 332 | ` 333 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/csv" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "log" 10 | "net/http" 11 | "os" 12 | "os/user" 13 | "path/filepath" 14 | "regexp" 15 | "strings" 16 | 17 | "github.com/landakram/plaid-cli/pkg/plaid_cli" 18 | "github.com/manifoldco/promptui" 19 | "github.com/plaid/plaid-go/plaid" 20 | "github.com/spf13/cobra" 21 | 22 | "github.com/spf13/viper" 23 | 24 | "github.com/Xuanwo/go-locale" 25 | "golang.org/x/text/language" 26 | ) 27 | 28 | func sliceToMap(slice []string) map[string]bool { 29 | set := make(map[string]bool, len(slice)) 30 | for _, s := range slice { 31 | set[s] = true 32 | } 33 | return set 34 | } 35 | 36 | // See https://plaid.com/docs/link/customization/#language-and-country 37 | var plaidSupportedCountries = []string{"US", "CA", "GB", "IE", "ES", "FR", "NL"} 38 | var plaidSupportedLanguages = []string{"en", "fr", "es", "nl"} 39 | 40 | func AreValidCountries(countries []string) bool { 41 | supportedCountries := sliceToMap(plaidSupportedCountries) 42 | for _, c := range countries { 43 | if !supportedCountries[c] { 44 | return false 45 | } 46 | } 47 | 48 | return true 49 | } 50 | 51 | func IsValidLanguageCode(lang string) bool { 52 | supportedLanguages := sliceToMap(plaidSupportedLanguages) 53 | return supportedLanguages[lang] 54 | } 55 | 56 | func main() { 57 | log.SetFlags(0) 58 | 59 | usr, _ := user.Current() 60 | dir := usr.HomeDir 61 | viper.SetDefault("cli.data_dir", filepath.Join(dir, ".plaid-cli")) 62 | 63 | dataDir := viper.GetString("cli.data_dir") 64 | data, err := plaid_cli.LoadData(dataDir) 65 | 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | 70 | viper.SetConfigName("config") 71 | viper.SetConfigType("toml") 72 | viper.AddConfigPath(dataDir) 73 | viper.AddConfigPath(".") 74 | err = viper.ReadInConfig() 75 | if err != nil { 76 | if _, ok := err.(viper.ConfigFileNotFoundError); ok { 77 | // Config file not found; ignore error if desired 78 | } else { 79 | log.Fatal(err) 80 | } 81 | } 82 | 83 | viper.SetEnvPrefix("") 84 | viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_", ".", "_")) 85 | viper.AutomaticEnv() 86 | 87 | tag, err := locale.Detect() 88 | if err != nil { 89 | tag = language.AmericanEnglish 90 | } 91 | 92 | region, _ := tag.Region() 93 | base, _ := tag.Base() 94 | 95 | var country string 96 | if region.IsCountry() { 97 | country = region.String() 98 | } else { 99 | country = "US" 100 | } 101 | 102 | lang := base.String() 103 | 104 | viper.SetDefault("plaid.countries", []string{country}) 105 | countriesOpt := viper.GetStringSlice("plaid.countries") 106 | var countries []string 107 | for _, c := range countriesOpt { 108 | countries = append(countries, strings.ToUpper(c)) 109 | } 110 | 111 | viper.SetDefault("plaid.language", lang) 112 | lang = viper.GetString("plaid.language") 113 | 114 | if !AreValidCountries(countries) { 115 | log.Fatalln("⚠️ Invalid countries. Please configure `plaid.countries` (using an envvar, PLAID_COUNTRIES, or in plaid-cli's config file) to a subset of countries that Plaid supports. Plaid supports the following countries: ", plaidSupportedCountries) 116 | } 117 | 118 | if !IsValidLanguageCode(lang) { 119 | log.Fatalln("⚠️ Invalid language code. Please configure `plaid.language` (using an envvar, PLAID_LANGUAGE, or in plaid-cli's config file) to a language that Plaid supports. Plaid supports the following languages: ", plaidSupportedLanguages) 120 | } 121 | 122 | viper.SetDefault("plaid.environment", "development") 123 | plaidEnvStr := strings.ToLower(viper.GetString("plaid.environment")) 124 | 125 | var plaidEnv plaid.Environment 126 | switch plaidEnvStr { 127 | case "development": 128 | plaidEnv = plaid.Development 129 | case "production": 130 | plaidEnv = plaid.Production 131 | default: 132 | log.Fatalln("Invalid plaid environment. Valid plaid environments are 'development' or 'production'.") 133 | } 134 | 135 | opts := plaid.ClientOptions{ 136 | viper.GetString("plaid.client_id"), 137 | viper.GetString("plaid.secret"), 138 | plaidEnv, 139 | &http.Client{}, 140 | } 141 | 142 | client, err := plaid.NewClient(opts) 143 | 144 | if err != nil { 145 | log.Fatal(err) 146 | } 147 | 148 | linker := plaid_cli.NewLinker(data, client, countries, lang) 149 | 150 | linkCommand := &cobra.Command{ 151 | Use: "link [ITEM-ID-OR-ALIAS]", 152 | Short: "Link an institution so plaid-cli can pull transactions", 153 | Long: "Link an institution so plaid-cli can pull transactions. An item ID or alias can be passed to initiate a relink.", 154 | Args: cobra.MaximumNArgs(1), 155 | Run: func(cmd *cobra.Command, args []string) { 156 | port := viper.GetString("link.port") 157 | 158 | var tokenPair *plaid_cli.TokenPair 159 | 160 | var err error 161 | 162 | if len(args) > 0 && len(args[0]) > 0 { 163 | itemOrAlias := args[0] 164 | 165 | itemID, ok := data.Aliases[itemOrAlias] 166 | if ok { 167 | itemOrAlias = itemID 168 | } 169 | 170 | err = linker.Relink(itemOrAlias, port) 171 | log.Println("Institution relinked!") 172 | return 173 | } else { 174 | tokenPair, err = linker.Link(port) 175 | if err != nil { 176 | log.Fatalln(err) 177 | } 178 | data.Tokens[tokenPair.ItemID] = tokenPair.AccessToken 179 | err = data.Save() 180 | } 181 | 182 | if err != nil { 183 | log.Fatalln(err) 184 | } 185 | 186 | log.Println("Institution linked!") 187 | log.Println(fmt.Sprintf("Item ID: %s", tokenPair.ItemID)) 188 | 189 | if alias, ok := data.BackAliases[tokenPair.ItemID]; ok { 190 | log.Println(fmt.Sprintf("Alias: %s", alias)) 191 | return 192 | } 193 | 194 | validate := func(input string) error { 195 | matched, err := regexp.Match(`^\w+$`, []byte(input)) 196 | if err != nil { 197 | return err 198 | } 199 | 200 | if !matched && input != "" { 201 | return errors.New("Valid characters: [0-9A-Za-z_]") 202 | } 203 | 204 | return nil 205 | } 206 | 207 | log.Println("You can give the institution a friendly alias and use that instead of the item ID in most commands.") 208 | prompt := promptui.Prompt{ 209 | Label: "Alias (default: none)", 210 | Validate: validate, 211 | } 212 | 213 | input, err := prompt.Run() 214 | if err != nil { 215 | log.Fatalln(err) 216 | } 217 | 218 | if input != "" { 219 | err = SetAlias(data, tokenPair.ItemID, input) 220 | if err != nil { 221 | log.Fatalln(err) 222 | } 223 | } 224 | }, 225 | } 226 | 227 | linkCommand.Flags().StringP("port", "p", "8080", "Port on which to serve Plaid Link") 228 | viper.BindPFlag("link.port", linkCommand.Flags().Lookup("port")) 229 | 230 | tokensCommand := &cobra.Command{ 231 | Use: "tokens", 232 | Short: "List access tokens", 233 | Run: func(cmd *cobra.Command, args []string) { 234 | resolved := make(map[string]string) 235 | for itemID, token := range data.Tokens { 236 | if alias, ok := data.BackAliases[itemID]; ok { 237 | resolved[alias] = token 238 | } else { 239 | resolved[itemID] = token 240 | } 241 | } 242 | 243 | printJSON, err := json.MarshalIndent(resolved, "", " ") 244 | if err != nil { 245 | log.Fatalln(err) 246 | } 247 | fmt.Println(string(printJSON)) 248 | }, 249 | } 250 | 251 | aliasCommand := &cobra.Command{ 252 | Use: "alias [ITEM-ID] [NAME]", 253 | Short: "Give a linked institution a friendly name", 254 | Long: "Give a linked institution a friendly name. You can use this name instead of the idem ID in most commands.", 255 | Args: cobra.ExactArgs(2), 256 | Run: func(cmd *cobra.Command, args []string) { 257 | itemID := args[0] 258 | alias := args[1] 259 | 260 | err := SetAlias(data, itemID, alias) 261 | if err != nil { 262 | log.Fatalln(err) 263 | } 264 | }, 265 | } 266 | 267 | aliasesCommand := &cobra.Command{ 268 | Use: "aliases", 269 | Short: "List aliases", 270 | Run: func(cmd *cobra.Command, args []string) { 271 | printJSON, err := json.MarshalIndent(data.Aliases, "", " ") 272 | if err != nil { 273 | log.Fatalln(err) 274 | } 275 | fmt.Println(string(printJSON)) 276 | }, 277 | } 278 | 279 | accountsCommand := &cobra.Command{ 280 | Use: "accounts [ITEM-ID-OR-ALIAS]", 281 | Short: "List accounts for a given institution", 282 | Long: "List accounts for a given institution. An account ID returned from this command can be used as a filter when listing transactions.", 283 | Args: cobra.ExactArgs(1), 284 | Run: func(cmd *cobra.Command, args []string) { 285 | itemOrAlias := args[0] 286 | itemID, ok := data.Aliases[itemOrAlias] 287 | if ok { 288 | itemOrAlias = itemID 289 | } 290 | 291 | err := WithRelinkOnAuthError(itemOrAlias, data, linker, func() error { 292 | token := data.Tokens[itemOrAlias] 293 | res, err := client.GetAccounts(token) 294 | if err != nil { 295 | return err 296 | } 297 | 298 | b, err := json.MarshalIndent(res.Accounts, "", " ") 299 | if err != nil { 300 | return err 301 | } 302 | 303 | fmt.Println(string(b)) 304 | 305 | return nil 306 | }) 307 | 308 | if err != nil { 309 | log.Fatalln(err) 310 | } 311 | }, 312 | } 313 | 314 | var fromFlag string 315 | var toFlag string 316 | var accountID string 317 | var outputFormat string 318 | transactionsCommand := &cobra.Command{ 319 | Use: "transactions [ITEM-ID-OR-ALIAS]", 320 | Short: "List transactions for a given institution", 321 | Args: cobra.ExactArgs(1), 322 | Run: func(cmd *cobra.Command, args []string) { 323 | itemOrAlias := args[0] 324 | itemID, ok := data.Aliases[itemOrAlias] 325 | if ok { 326 | itemOrAlias = itemID 327 | } 328 | 329 | err := WithRelinkOnAuthError(itemOrAlias, data, linker, func() error { 330 | token := data.Tokens[itemOrAlias] 331 | 332 | var accountIDs []string 333 | if len(accountID) > 0 { 334 | accountIDs = append(accountIDs, accountID) 335 | } 336 | 337 | options := plaid.GetTransactionsOptions{ 338 | StartDate: fromFlag, 339 | EndDate: toFlag, 340 | AccountIDs: accountIDs, 341 | Count: 100, 342 | Offset: 0, 343 | } 344 | 345 | transactions, err := AllTransactions(options, client, token) 346 | if err != nil { 347 | return err 348 | } 349 | 350 | serializer, err := NewTransactionSerializer(outputFormat) 351 | if err != nil { 352 | return err 353 | } 354 | 355 | b, err := serializer.serialize(transactions) 356 | if err != nil { 357 | return err 358 | } 359 | 360 | fmt.Println(string(b)) 361 | 362 | return nil 363 | }) 364 | 365 | if err != nil { 366 | log.Fatalln(err) 367 | } 368 | }, 369 | } 370 | transactionsCommand.Flags().StringVarP(&fromFlag, "from", "f", "", "Date of first transaction (required)") 371 | transactionsCommand.MarkFlagRequired("from") 372 | 373 | transactionsCommand.Flags().StringVarP(&toFlag, "to", "t", "", "Date of last transaction (required)") 374 | transactionsCommand.MarkFlagRequired("to") 375 | 376 | transactionsCommand.Flags().StringVarP(&outputFormat, "output-format", "o", "json", "Output format") 377 | transactionsCommand.Flags().StringVarP(&accountID, "account-id", "a", "", "Fetch transactions for this account ID only.") 378 | 379 | var withStatusFlag bool 380 | var withOptionalMetadataFlag bool 381 | insitutionCommand := &cobra.Command{ 382 | Use: "institution [ITEM-ID-OR-ALIAS]", 383 | Short: "Get information about an institution", 384 | Long: "Get information about an institution. Status can be reported using a flag.", 385 | Args: cobra.ExactArgs(1), 386 | Run: func(cmd *cobra.Command, args []string) { 387 | itemOrAlias := args[0] 388 | itemID, ok := data.Aliases[itemOrAlias] 389 | if ok { 390 | itemOrAlias = itemID 391 | } 392 | 393 | err := WithRelinkOnAuthError(itemOrAlias, data, linker, func() error { 394 | token := data.Tokens[itemOrAlias] 395 | 396 | itemResp, err := client.GetItem(token) 397 | if err != nil { 398 | return err 399 | } 400 | 401 | instID := itemResp.Item.InstitutionID 402 | 403 | opts := plaid.GetInstitutionByIDOptions{ 404 | IncludeOptionalMetadata: withOptionalMetadataFlag, 405 | IncludeStatus: withStatusFlag, 406 | } 407 | resp, err := client.GetInstitutionByIDWithOptions(instID, countries, opts) 408 | if err != nil { 409 | return err 410 | } 411 | 412 | b, err := json.MarshalIndent(resp.Institution, "", " ") 413 | if err != nil { 414 | return err 415 | } 416 | 417 | fmt.Println(string(b)) 418 | 419 | return nil 420 | }) 421 | 422 | if err != nil { 423 | log.Fatalln(err) 424 | } 425 | }, 426 | } 427 | insitutionCommand.Flags().BoolVarP(&withStatusFlag, "status", "s", false, "Fetch institution status") 428 | insitutionCommand.Flags().BoolVarP(&withOptionalMetadataFlag, "optional-metadata", "m", false, "Fetch optional metadata like logo and URL") 429 | 430 | rootCommand := &cobra.Command{ 431 | Use: "plaid-cli", 432 | Short: "Link bank accounts and get transactions from the command line.", 433 | Long: `plaid-cli 🤑 434 | 435 | plaid-cli is a CLI tool for working with the Plaid API. 436 | 437 | You can use plaid-cli to link bank accounts and pull transactions in multiple 438 | output formats from the comfort of the command line. 439 | 440 | Configuration: 441 | To get started, you'll need Plaid API credentials, which you can get by visiting 442 | https://dashboard.plaid.com/team/keys after signing up for free. 443 | 444 | plaid-cli will look at the following environment variables for API credentials: 445 | 446 | PLAID_CLIENT_ID= 447 | PLAID_SECRET= 448 | PLAID_ENVIRONMENT=development 449 | PLAID_LANGUAGE=en # optional, detected using system's locale 450 | PLAID_COUNTRIES=US # optional, detected using system's locale 451 | 452 | I recommend setting and exporting these on shell startup. 453 | 454 | API credentials can also be specified using a config file located at 455 | ~/.plaid-cli/config.toml: 456 | 457 | [plaid] 458 | client_id = "" 459 | secret = "" 460 | environment = "development" 461 | 462 | After setting those API credentials, plaid-cli is ready to use! 463 | You'll probably want to run 'plaid-cli link' next. 464 | 465 | Please see the README (https://github.com/landakram/plaid-cli/blob/master/README.md) 466 | for more detailed usage instructions. 467 | 468 | Made by @landakram. 469 | `, 470 | } 471 | rootCommand.AddCommand(linkCommand) 472 | rootCommand.AddCommand(tokensCommand) 473 | rootCommand.AddCommand(aliasCommand) 474 | rootCommand.AddCommand(aliasesCommand) 475 | rootCommand.AddCommand(accountsCommand) 476 | rootCommand.AddCommand(transactionsCommand) 477 | rootCommand.AddCommand(insitutionCommand) 478 | 479 | if !viper.IsSet("plaid.client_id") { 480 | log.Println("⚠️ PLAID_CLIENT_ID not set. Please see the configuration instructions below.") 481 | rootCommand.Help() 482 | os.Exit(1) 483 | } 484 | if !viper.IsSet("plaid.secret") { 485 | log.Println("⚠️ PLAID_SECRET not set. Please see the configuration instructions below.") 486 | rootCommand.Help() 487 | os.Exit(1) 488 | } 489 | 490 | rootCommand.Execute() 491 | } 492 | 493 | func AllTransactions(opts plaid.GetTransactionsOptions, client *plaid.Client, token string) ([]plaid.Transaction, error) { 494 | var transactions []plaid.Transaction 495 | 496 | res, err := client.GetTransactionsWithOptions(token, opts) 497 | if err != nil { 498 | return transactions, err 499 | } 500 | 501 | transactions = append(transactions, res.Transactions...) 502 | 503 | for len(transactions) < res.TotalTransactions { 504 | opts.Offset += opts.Count 505 | res, err := client.GetTransactionsWithOptions(token, opts) 506 | if err != nil { 507 | return transactions, err 508 | } 509 | 510 | transactions = append(transactions, res.Transactions...) 511 | 512 | } 513 | 514 | return transactions, nil 515 | } 516 | 517 | func WithRelinkOnAuthError(itemID string, data *plaid_cli.Data, linker *plaid_cli.Linker, action func() error) error { 518 | err := action() 519 | if e, ok := err.(plaid.Error); ok { 520 | if e.ErrorCode == "ITEM_LOGIN_REQUIRED" { 521 | log.Println("Login expired. Relinking...") 522 | 523 | port := viper.GetString("link.port") 524 | 525 | err = linker.Relink(itemID, port) 526 | 527 | if err != nil { 528 | return err 529 | } 530 | 531 | log.Println("Re-running action...") 532 | 533 | err = action() 534 | } 535 | } 536 | 537 | return err 538 | } 539 | 540 | type TransactionSerializer interface { 541 | serialize(txs []plaid.Transaction) ([]byte, error) 542 | } 543 | 544 | func NewTransactionSerializer(t string) (TransactionSerializer, error) { 545 | switch t { 546 | case "csv": 547 | return &CSVSerializer{}, nil 548 | case "json": 549 | return &JSONSerializer{}, nil 550 | default: 551 | return nil, errors.New(fmt.Sprintf("Invalid output format: %s", t)) 552 | } 553 | } 554 | 555 | type CSVSerializer struct{} 556 | 557 | func (w *CSVSerializer) serialize(txs []plaid.Transaction) ([]byte, error) { 558 | var records [][]string 559 | for _, tx := range txs { 560 | sanitizedName := strings.ReplaceAll(tx.Name, ",", "") 561 | records = append(records, []string{tx.Date, fmt.Sprintf("%f", tx.Amount), sanitizedName}) 562 | } 563 | 564 | b := bytes.NewBufferString("") 565 | writer := csv.NewWriter(b) 566 | err := writer.Write([]string{"Date", "Amount", "Description"}) 567 | if err != nil { 568 | return nil, err 569 | } 570 | err = writer.WriteAll(records) 571 | if err != nil { 572 | return nil, err 573 | } 574 | 575 | return b.Bytes(), err 576 | } 577 | 578 | func SetAlias(data *plaid_cli.Data, itemID string, alias string) error { 579 | if _, ok := data.Tokens[itemID]; !ok { 580 | return errors.New(fmt.Sprintf("No access token found for item ID `%s`. Try re-linking your account with `plaid-cli link`.", itemID)) 581 | } 582 | 583 | data.Aliases[alias] = itemID 584 | data.BackAliases[itemID] = alias 585 | err := data.Save() 586 | if err != nil { 587 | return err 588 | } 589 | 590 | log.Println(fmt.Sprintf("Aliased %s to %s.", itemID, alias)) 591 | 592 | return nil 593 | } 594 | 595 | type JSONSerializer struct{} 596 | 597 | func (w *JSONSerializer) serialize(txs []plaid.Transaction) ([]byte, error) { 598 | return json.MarshalIndent(txs, "", " ") 599 | } 600 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 7 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 8 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 9 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 10 | cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= 11 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 12 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 13 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 14 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 15 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 16 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 17 | github.com/Xuanwo/go-locale v1.0.0 h1:oqC32Kyiu2XZq+fxtwEg0mWiv9WyDhyHu+sT5cDkgME= 18 | github.com/Xuanwo/go-locale v1.0.0/go.mod h1:kB9tcLfr4Sp+ByIE9SE7vbUkXkGQqel2XH3EHpL0haA= 19 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 20 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 21 | github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= 22 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 23 | github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= 24 | github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 25 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 26 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 27 | github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= 28 | github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= 29 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 30 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 31 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 32 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 33 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 34 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 35 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 36 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 37 | github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 38 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 39 | github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 40 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 41 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 42 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 43 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 44 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 45 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 46 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 48 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 49 | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= 50 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= 51 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 52 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 53 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 54 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 55 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 56 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 57 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 58 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 59 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 60 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 61 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 62 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 63 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 64 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 65 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 66 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 67 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 68 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 69 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 70 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 71 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 72 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 73 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 74 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 75 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 76 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 77 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 78 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 79 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 80 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 81 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 82 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 83 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 84 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 85 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 86 | github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= 87 | github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= 88 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 89 | github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 90 | github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 91 | github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 92 | github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= 93 | github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= 94 | github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= 95 | github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= 96 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 97 | github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 98 | github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= 99 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 100 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 101 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 102 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 103 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 104 | github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= 105 | github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= 106 | github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= 107 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 108 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 109 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 110 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 111 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 112 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 113 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 114 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= 115 | github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 116 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 117 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 118 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 119 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 120 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 121 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 122 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 123 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 124 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= 125 | github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 126 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 127 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 128 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 129 | github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= 130 | github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= 131 | github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= 132 | github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 133 | github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 134 | github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= 135 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 136 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 137 | github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 138 | github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= 139 | github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 140 | github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= 141 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 142 | github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= 143 | github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= 144 | github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= 145 | github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 146 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= 147 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 148 | github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= 149 | github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 150 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 151 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 152 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 153 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 154 | github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= 155 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= 156 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 157 | github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMFtw= 158 | github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= 159 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 160 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 161 | github.com/plaid/plaid-go v0.0.0-20200529200923-9627743aa512 h1:XmKvXLOGi20FfHv20HQYhct0y4vLOcSaT+cMz4MIDw0= 162 | github.com/plaid/plaid-go v0.0.0-20200529200923-9627743aa512/go.mod h1:c7cDT1Lkcr0AgKJGVIG+oCa07jOrrg4Um8nduQ1eQN0= 163 | github.com/plaid/plaid-go v0.0.0-20210112002311-0cf0e6f0ea3e h1:8uVgUfPCS63Ys/8BawzsnDZem8NQsCUlo2yKO0uSqac= 164 | github.com/plaid/plaid-go v0.0.0-20210112002311-0cf0e6f0ea3e/go.mod h1:c7cDT1Lkcr0AgKJGVIG+oCa07jOrrg4Um8nduQ1eQN0= 165 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 166 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 167 | github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= 168 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 169 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 170 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 171 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 172 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 173 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 174 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 175 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 176 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 177 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 178 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 179 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 180 | github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= 181 | github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= 182 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 183 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 184 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 185 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 186 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 187 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 188 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 189 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 190 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 191 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 192 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= 193 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 194 | github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= 195 | github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= 196 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= 197 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 198 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 199 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 200 | github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= 201 | github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 202 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= 203 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 204 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 205 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 206 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= 207 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 208 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 209 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 210 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 211 | github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM= 212 | github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= 213 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 214 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 215 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 216 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 217 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 218 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 219 | github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= 220 | github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= 221 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 222 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 223 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 224 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 225 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 226 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 227 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 228 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 229 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 230 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 231 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 232 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 233 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 234 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 235 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 236 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 237 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 238 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 239 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 240 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 241 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 242 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 243 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 244 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 245 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 246 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 247 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 248 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 249 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 250 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 251 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 252 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 253 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 254 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 255 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 256 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 257 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 258 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 259 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 260 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 261 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 262 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 263 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 264 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 265 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 266 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 267 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 268 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 269 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 270 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 271 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 272 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 273 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 274 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 275 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 276 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 277 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 278 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 279 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 280 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 281 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 282 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 283 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 284 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 285 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 286 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 287 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 288 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 289 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 290 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 h1:HyfiK1WMnHj5FXFXatD+Qs1A/xC2Run6RzeW1SyHxpc= 291 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 292 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 293 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y= 294 | golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 295 | golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3 h1:qDJKu1y/1SjhWac4BQZjLljqvqiWUhjmDMnonmVGDAU= 296 | golang.org/x/sys v0.0.0-20200802091954-4b90ce9b60b3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 298 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 299 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 300 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 301 | golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= 302 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 303 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 304 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 305 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 306 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 307 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 308 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 309 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 310 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 311 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 312 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 313 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 314 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 315 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 316 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 317 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 318 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 319 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 320 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 321 | golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 322 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 323 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 324 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 325 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 326 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 327 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 328 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 329 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 330 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 331 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 332 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 333 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 334 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 335 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 336 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 337 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 338 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 339 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 340 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 341 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 342 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 343 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 344 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 345 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 346 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 347 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 348 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 349 | gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= 350 | gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 351 | gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww= 352 | gopkg.in/ini.v1 v1.57.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 353 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 354 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 355 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 356 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 357 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 358 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 359 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 360 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 361 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 362 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 363 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 364 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 365 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 366 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 367 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 368 | --------------------------------------------------------------------------------