diff --git a/.vscode/launch.json b/.vscode/launch.json index 757d8ee..d8729d5 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -29,6 +29,18 @@ "~/.ma/pong.yaml" ], "console": "integratedTerminal" + }, + { + "name": "Show config", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "${fileDirname}", + "args": [ + "--show_config", + ], + "console": "integratedTerminal" } + ] } \ No newline at end of file diff --git a/README.md b/README.md index 4fb5e58..c26c772 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,24 @@ This is go-ma-actor based on [an example from go-libp2p][src]. -Now you can either run with `go run`, or build and run the binary: +## Requirements + +This is a distributed app that relies heavily on the [libp2p](https://libp2p.io/) stack +and [IPFS][ipfs] in particular. It's unusable unless you have a running IPFS node. + +I suggest using [Brave Browser][brave] or [IPFS Desktop][desktop] to run and IPFS node. + +*By using Brave browser your can run an IPFS node without installing anything. +And you can investigate the IPFS network with the built-in IPFS node. +It provides The ability to browse IPFS properly, and to pin files and directories.* + +## TL;DR ```bash -# Generate persistent environment variables of *SECRET* keysets -eval $(go run . -genenv -forcePublish | tee .env) + +# Generate persistent config file with *SECRETS* +# It needs to be published to the IPFS network to be useful +./go-ma-actor --generate --nick "asj" --publish > actor.yaml ./go-ma-actor # Share and enjoy! ``` @@ -16,30 +29,17 @@ type `./go-ma-actor -help`. Most config settings can be set with environment var ```bash export GO_MA_LOG_LEVEL="error" -export GO_MA_DISCOVERY_TIMEOUT="300" -export GO_MA_KEYSET="myBase58EncodedPrivkeyGeneratedByGenerate" +export GO_MA_LIBP2P_DISCOVERY_TIMEOUT="300" +export GO_MA_ACTOR_IDENTITY="myBase58EncodedPrivkeyGeneratedByGenerate" ``` ## Identity -A `-generate` or `genenv` parameter to generate a text version of a secret key. -The key is text formatted privKey for your node. - -This key can and should be kept safely on a PostIt note on your monitor :-) -Just don't store somewhere insecure. It's your future identity. - -```bash -unset HISTFILE - export GO_MA_ACTOR_KEYSET=FooBarABCDEFbase58 -``` - -or specified on the command line: - -```bash -./go-ma-actor -keyset FooBarABCDEFbase58 -``` +A `-generate` flag is available to generate a new identity. +It uses defaults, BUT it generates a new random identity. -The first is the best. (Noticed that in most shells the empty space before the command, means that the line isn't saved in history.) +You can use the output as your future identity, but keep it secret. +Those identities are used to sign messages, and to encrypt and decrypt private messages. ## Usage @@ -49,8 +49,14 @@ To quit, hit `Ctrl-C`, or type `/quit` into the input field. - /status [sub|topic|host] - /discover +- /alias [node|entity] set [DID|NAME] NAME +- /aliases +- /whereis [DID|NAME] +- /msg Name Message - /enter room -- /nick Name - /refresh [src]: https://github.com/libp2p/go-libp2p/tree/master/examples/pubsub/chat +[brave]: (Recommended Browser for 間) +[desktop]: (IPFS Desktop) +[ipfs]: (IPFS) diff --git a/alias/alias.go b/alias/alias.go index 8888ea3..e61ac9c 100644 --- a/alias/alias.go +++ b/alias/alias.go @@ -10,22 +10,19 @@ import ( "github.com/libp2p/go-libp2p/core/peer" _ "github.com/mattn/go-sqlite3" log "github.com/sirupsen/logrus" - "github.com/spf13/pflag" - "github.com/spf13/viper" ) const ( - defaultAliasFile = "~/.ma/aliases.db" defaultAliasLength = 8 - SELECT_ENTITY_NICK = "SELECT nick FROM entities WHERE did = ?" - SELECT_ENTITY_DID = "SELECT did FROM entities WHERE nick = ?" - SELECT_NODE_NICK = "SELECT nick FROM nodes WHERE id = ?" - SELECT_NODE_ID = "SELECT id FROM nodes WHERE nick = ?" - UPSERT_ENTITY = "INSERT INTO entities (did, nick) VALUES (?, ?) ON CONFLICT(did) DO UPDATE SET nick = ?" - UPSERT_NODE = "INSERT INTO nodes (id, nick) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET nick = ?" - DELETE_ENTITY = "DELETE FROM entities WHERE did = ?" - DELETE_NODE = "DELETE FROM nodes WHERE id = ?" + _SELECT_ENTITY_NICK = "SELECT nick FROM entities WHERE did = ?" + _SELECT_ENTITY_DID = "SELECT did FROM entities WHERE nick = ?" + _SELECT_NODE_NICK = "SELECT nick FROM nodes WHERE id = ?" + _SELECT_NODE_ID = "SELECT id FROM nodes WHERE nick = ?" + _UPSERT_ENTITY = "INSERT INTO entities (did, nick) VALUES (?, ?) ON CONFLICT(did) DO UPDATE SET nick = ?" + _UPSERT_NODE = "INSERT INTO nodes (id, nick) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET nick = ?" + _DELETE_ENTITY = "DELETE FROM entities WHERE did = ?" + _DELETE_NODE = "DELETE FROM nodes WHERE id = ?" ) var ( @@ -33,13 +30,6 @@ var ( db *sql.DB ) -func init() { - - pflag.String("aliases", defaultAliasFile, "File to *write* node aliases to. If the file does not exist, it will be created.") - viper.BindPFlag("aliases", pflag.Lookup("aliases")) - viper.SetDefault("aliases", defaultAliasFile) -} - // Initiates the database connection and creates the tables if they do not exist func GetDB() (*sql.DB, error) { @@ -89,7 +79,7 @@ func GetEntityAlias(id string) (string, error) { var a string - err = db.QueryRow(SELECT_ENTITY_NICK, id).Scan(&a) + err = db.QueryRow(_SELECT_ENTITY_NICK, id).Scan(&a) if err != nil { return "", err } @@ -112,7 +102,7 @@ func GetEntityDID(nick string) (string, error) { var id string - err = db.QueryRow(SELECT_ENTITY_DID, nick).Scan(&id) + err = db.QueryRow(_SELECT_ENTITY_DID, nick).Scan(&id) if err != nil { return "", err } @@ -136,7 +126,7 @@ func GetNodeAlias(id string) (string, error) { var a string - err = db.QueryRow(SELECT_NODE_NICK, id).Scan(&a) + err = db.QueryRow(_SELECT_NODE_NICK, id).Scan(&a) if err != nil { return "", err } @@ -159,7 +149,7 @@ func GetNodeID(nick string) (string, error) { var id string - err = db.QueryRow(SELECT_NODE_ID, nick).Scan(&id) + err = db.QueryRow(_SELECT_NODE_ID, nick).Scan(&id) if err != nil { return "", err } @@ -180,7 +170,7 @@ func SetEntityAlias(id string, nick string) error { return err } - _, err = db.Exec(UPSERT_ENTITY, id, nick, nick) + _, err = db.Exec(_UPSERT_ENTITY, id, nick, nick) if err != nil { return err } @@ -202,7 +192,7 @@ func SetNodeAlias(id string, nick string) error { return err } - _, err = db.Exec(UPSERT_NODE, id, nick, nick) + _, err = db.Exec(_UPSERT_NODE, id, nick, nick) if err != nil { return err } @@ -222,7 +212,7 @@ func RemoveEntityAlias(id string) error { return err } - _, err = db.Exec(DELETE_ENTITY, id) + _, err = db.Exec(_DELETE_ENTITY, id) if err != nil { return err } diff --git a/config/actor.go b/config/actor.go index e80e190..6fdd333 100644 --- a/config/actor.go +++ b/config/actor.go @@ -66,15 +66,25 @@ func InitActor() { } -func handleGenerateOrExit() { +// Genreates a libp2p and actor identity and returns the keyset and the actor identity +// These are imperative, so failure to generate them is a fatal error. +func handleGenerateOrExit() (string, string) { + // Generate a new keysets if requested + nick := viper.GetString("actor.nick") - keyset_string, err := generateAndPrintActorIdentity() + keyset_string, err := generateKeysetString(nick) if err != nil { log.Errorf("config.initIdentity: Failed to generate keyset: %v", err) os.Exit(70) // EX_SOFTWARE } + ni, err := generateNodeIdentity() + if err != nil { + log.Errorf("config.initIdentity: Failed to generate node identity: %v", err) + os.Exit(70) // EX_SOFTWARE + } + if viper.GetBool("publish") { err = publishActorIdentityFromString(keyset_string) if err != nil { @@ -83,26 +93,7 @@ func handleGenerateOrExit() { } } - err = generateAndPrintNodeIdentity() - if err != nil { - log.Errorf("config.initIdentity: Failed to generate node identity: %v", err) - os.Exit(70) // EX_SOFTWARE - } - -} - -func generateAndPrintActorIdentity() (string, error) { - - nick := viper.GetString("actor.nick") - - keyset_string, err := generateKeyset(nick) - if err != nil { - return "", fmt.Errorf("config.initIdentity: Failed to generate keyset: %v", err) - } - - fmt.Println(ENV_PREFIX + "_ACTOR_IDENTITY=" + keyset_string) - - return keyset_string, nil + return keyset_string, ni } func publishActorIdentityFromString(keyset_string string) error { @@ -120,7 +111,8 @@ func publishActorIdentityFromString(keyset_string string) error { return nil } -func generateKeyset(nick string) (string, error) { +// Generates a new keyset and returns the keyset as a string +func generateKeysetString(nick string) (string, error) { ks, err := set.GetOrCreate(nick) if err != nil { diff --git a/config/alias.go b/config/alias.go index 8498c0a..1d9a204 100644 --- a/config/alias.go +++ b/config/alias.go @@ -2,9 +2,19 @@ package config import ( "github.com/mitchellh/go-homedir" + "github.com/spf13/pflag" "github.com/spf13/viper" ) +const defaultAliases = "~/.ma/aliases.db" + +func init() { + + pflag.String("aliases", defaultAliases, "File to *write* node aliases to. If the file does not exist, it will be created.") + viper.BindPFlag("aliases", pflag.Lookup("aliases")) + viper.SetDefault("aliases", defaultAliases) +} + // Returns expanded path to the aliases file // If the expansion fails it returns an empty string func GetAliases() string { diff --git a/config/config.go b/config/config.go index 329ff54..d3d217a 100644 --- a/config/config.go +++ b/config/config.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/spf13/pflag" "github.com/spf13/viper" + "gopkg.in/yaml.v2" ) const ( @@ -32,6 +33,10 @@ func init() { // Allow to set config file via command line flag. pflag.StringVarP(&configFile, "config", "c", "", "Config file to use.") + pflag.Bool("show-config", false, "Whether to print the config.") + viper.BindPFlag("show-config", pflag.Lookup("show-config")) + pflag.Bool("show-defaults", false, "Whether to print the config.") + viper.BindPFlag("show-defaults", pflag.Lookup("show-defaults")) pflag.BoolP("version", "v", false, "Print version and exit.") viper.BindPFlag("version", pflag.Lookup("version")) @@ -62,7 +67,27 @@ func Init(configName string) error { // This will exit when done. It will also publish if applicable. if viper.GetBool("generate") { log.Info("Generating new keyset and node identity") - handleGenerateOrExit() + actor, node := handleGenerateOrExit() + generateConfigFile(actor, node) + os.Exit(0) + } + + if viper.GetBool("show-config") { + configMap := viper.AllSettings() + configYAML, err := yaml.Marshal(configMap) + if err != nil { + log.Fatalf("error: %v", err) + } + + // Print the YAML to stdout or write it to a template file + fmt.Println(string(configYAML)) + os.Exit(0) + } + + if viper.GetBool("show-defaults") { + + // Print the YAML to stdout or write it to a template file + generateConfigFile("zNO_DEFAULT_ACTOR_IDENITY", "zNO_DEFAULT_NODE_IDENITY") os.Exit(0) } diff --git a/config/generate.go b/config/generate.go new file mode 100644 index 0000000..9c56d60 --- /dev/null +++ b/config/generate.go @@ -0,0 +1,63 @@ +package config + +import ( + "fmt" + + "github.com/bahner/go-ma" + "github.com/bahner/go-ma/key/set" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +// Well, actually find a nice one for this! +const defaultHome = "did:ma:k2k4r8kzkhamrqz9m5yy0tihj1fso3t6znnuidu00dbtnh3plazatrfw#pong" + +func generateConfigFile(actor string, node string) { + + ks, err := set.Unpack(actor) + if err != nil { + log.Fatalf("error: %v", err) + } + + // Get the default settings as a map + // Note: Viper does not have a built-in way to directly extract only the defaults + // so we manually recreate the structure based on the defaults we have set. + defaults := map[string]interface{}{ + "actor": map[string]interface{}{ + "identity": actor, + "home": defaultHome, + }, + "aliases": defaultAliases, + "log": map[string]interface{}{ + "level": defaultLogLevel, + "file": defaultLogfile, + }, + "api": map[string]interface{}{ + "maddr": ma.DEFAULT_IPFS_API_MULTIADDR, + }, + "http": map[string]interface{}{ + "socket": defaultHttpSocket, + }, + "libp2p": map[string]interface{}{ + "identity": node, + "port": defaultListenPort, + "connmgr": map[string]interface{}{ + "low-watermark": defaultConnmgrLowWatermark, + "high-watermark": defaultConnmgrHighWatermark, + "grace-period": defaultConnmgrGracePeriod, + }, + "discovery-retry": defaultDiscoveryRetryInterval, + "discovery-timeout": defaultDiscoveryTimeout, + }, + } + + // Convert the map of defaults to YAML + defaultsYAML, err := yaml.Marshal(defaults) + if err != nil { + log.Fatalf("error: %v", err) + } + + // Print the YAML defaults + fmt.Println("# " + ks.DID.String()) + fmt.Println(string(defaultsYAML)) +} diff --git a/config/logging.go b/config/logging.go index e5e07df..6839c8e 100644 --- a/config/logging.go +++ b/config/logging.go @@ -17,13 +17,13 @@ const ( func init() { - pflag.String("loglevel", defaultLogLevel, "Loglevel to use for application.") + pflag.String("log-level", defaultLogLevel, "Loglevel to use for application.") viper.SetDefault("log.level", defaultLogLevel) - viper.BindPFlag("log.level", pflag.Lookup("loglevel")) + viper.BindPFlag("log.level", pflag.Lookup("log-level")) - pflag.String("logfile", defaultLogfile, "Logfile to use for application. Accepts 'STDERR' and 'STDOUT' as such.") + pflag.String("log-file", defaultLogfile, "Logfile to use for application. Accepts 'STDERR' and 'STDOUT' as such.") viper.SetDefault("log.file", defaultLogfile) - viper.BindPFlag("log.file", pflag.Lookup("logfile")) + viper.BindPFlag("log.file", pflag.Lookup("log-file")) } func InitLogging() { diff --git a/config/p2p.go b/config/p2p.go index 8ee381f..a047dad 100644 --- a/config/p2p.go +++ b/config/p2p.go @@ -14,45 +14,41 @@ import ( ) const ( - defaultLowWaterMark int = 10 - defaultHighWaterMark int = 30 - defaultListenPort int = 0 + defaultConnmgrLowWatermark int = 10 + defaultConnmgrHighWatermark int = 30 + defaultListenPort int = 0 defaultDiscoveryTimeout time.Duration = time.Second * 20 - defaultConnMgrGrace time.Duration = time.Minute * 1 + defaultConnmgrGracePeriod time.Duration = time.Minute * 1 defaultDiscoveryRetryInterval time.Duration = time.Second * 300 ) func init() { // P2P Settings - pflag.Int("low_watermark", defaultLowWaterMark, "Low watermark for peer discovery.") - viper.SetDefault("libp2p.connmgr.low_watermark", defaultLowWaterMark) - viper.BindPFlag("libp2p.connmgr.low_watermark", pflag.Lookup("low_watermark")) + pflag.Int("low-watermark", defaultConnmgrLowWatermark, "Low watermark for peer discovery.") + viper.SetDefault("libp2p.connmgr.low-watermark", defaultConnmgrLowWatermark) + viper.BindPFlag("libp2p.connmgr.low-watermark", pflag.Lookup("low-watermark")) - pflag.Int("high_watermark", defaultHighWaterMark, "High watermark for peer discovery.") - viper.SetDefault("libp2p.connmgr.high_watermark", defaultHighWaterMark) - viper.BindPFlag("libp2p.connmgr.high_watermark", pflag.Lookup("high_watermark")) + pflag.Int("high-watermark", defaultConnmgrHighWatermark, "High watermark for peer discovery.") + viper.SetDefault("libp2p.connmgr.high-watermark", defaultConnmgrHighWatermark) + viper.BindPFlag("libp2p.connmgr.high-watermark", pflag.Lookup("high-watermark")) - // pflag.Int("desired_peers", defaultDesiredPeers, "Desired number of peers to connect to.") - // viper.SetDefault("libp2p.connmgr.desired_peers", defaultDesiredPeers) - // viper.BindPFlag("libp2p.connmgr.desired_peers", pflag.Lookup("desired_peers")) + pflag.Duration("grace-period", defaultConnmgrGracePeriod, "Grace period for connection manager.") + viper.SetDefault("libp2p.connmgr.grace-period", defaultConnmgrGracePeriod) + viper.BindPFlag("libp2p.connmgr.grace-period", pflag.Lookup("grace-period")) - pflag.Duration("grace_period", defaultConnMgrGrace, "Grace period for connection manager.") - viper.SetDefault("libp2p.connmgr.grace_period", defaultConnMgrGrace) - viper.BindPFlag("libp2p.connmgr.grace_period", pflag.Lookup("grace_period")) + pflag.Duration("discovery-retry", defaultDiscoveryRetryInterval, "Retry interval for peer discovery.") + viper.SetDefault("libp2p.discovery-retry", defaultDiscoveryRetryInterval) + viper.BindPFlag("libp2p.discovery-retry", pflag.Lookup("discovery-retryl")) - pflag.Duration("discovery_retry", defaultDiscoveryRetryInterval, "Retry interval for peer discovery.") - viper.SetDefault("libp2p.discovery_retry", defaultDiscoveryRetryInterval) - viper.BindPFlag("libp2p.discovery_retry", pflag.Lookup("discovery_retryl")) + pflag.Duration("discovery-timeout", defaultDiscoveryTimeout, "Timeout for peer discovery.") + viper.SetDefault("libp2p.discovery-timeout", defaultDiscoveryTimeout) + viper.BindPFlag("libp2p.discovery-timeout", pflag.Lookup("discoveryTimeout")) - pflag.Duration("discovery_timeout", defaultDiscoveryTimeout, "Timeout for peer discovery.") - viper.SetDefault("libp2p.discovery_timeout", defaultDiscoveryTimeout) - viper.BindPFlag("libp2p.connmgr.discovery_timeout", pflag.Lookup("discoveryTimeout")) - - pflag.Int("listen_port", defaultListenPort, "Port for libp2p node to listen on.") + pflag.Int("listen-port", defaultListenPort, "Port for libp2p node to listen on.") viper.SetDefault("libp2p.port", defaultListenPort) - viper.BindPFlag("libp2p.port", pflag.Lookup("listen_port")) + viper.BindPFlag("libp2p.port", pflag.Lookup("listen-port")) } // P2P Node identity @@ -94,18 +90,6 @@ func GetNodeIdentity() crypto.PrivKey { } -func generateAndPrintNodeIdentity() error { - - p2pPrivKey, err := generateNodeIdentity() - if err != nil { - return fmt.Errorf("config.initIdentity: Failed to generate node identity: %v", err) - } - - fmt.Println(ENV_PREFIX + "_LIBP2P_IDENTITY=" + p2pPrivKey) - - return nil -} - func generateNodeIdentity() (string, error) { pk, _, err := crypto.GenerateKeyPair(crypto.Ed25519, -1) if err != nil { @@ -141,7 +125,7 @@ func GetDiscoveryContext() (context.Context, func()) { } func GetDiscoveryTimeout() time.Duration { - return time.Duration(viper.GetDuration("libp2p.discovery_timeout")) + return time.Duration(viper.GetDuration("libp2p.discovery-timeout")) } func GetDiscoveryTimeoutString() string { @@ -149,7 +133,7 @@ func GetDiscoveryTimeoutString() string { } func GetLowWatermark() int { - return viper.GetInt("libp2p.connmgr.low_watermark") + return viper.GetInt("libp2p.connmgr.low-watermark") } func GetLowWatermarkString() string { @@ -157,7 +141,7 @@ func GetLowWatermarkString() string { } func GetHighWatermark() int { - return viper.GetInt("libp2p.connmgr.high_watermark") + return viper.GetInt("libp2p.connmgr.high-watermark") } func GetHighWatermarkString() string { @@ -165,7 +149,7 @@ func GetHighWatermarkString() string { } func GetConnMgrGracePeriod() time.Duration { - return viper.GetDuration("libp2p.connmgr.grace_period") + return viper.GetDuration("libp2p.connmgr.grace-period") } func GetConnMgrGraceString() string { @@ -181,7 +165,7 @@ func GetListenPortString() string { } func GetDiscoveryRetryInterval() time.Duration { - return viper.GetDuration("libp2p.discovery_retry") + return viper.GetDuration("libp2p.discovery-retry") } func GetDiscoveryRetryIntervalString() string { diff --git a/config/web.go b/config/web.go index 7b9c4b3..26b5a85 100644 --- a/config/web.go +++ b/config/web.go @@ -13,8 +13,8 @@ func init() { // Flags - user configurations - pflag.String("http_socket", defaultHttpSocket, "Address for webserver to listen on") - viper.BindPFlag("http.socket", pflag.Lookup("socket")) + pflag.String("http-socket", defaultHttpSocket, "Address for webserver to listen on") + viper.BindPFlag("http.socket", pflag.Lookup("http-socket")) } diff --git a/go.mod b/go.mod index ac3e329..f916f90 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bahner/go-ma-actor go 1.21 require ( - github.com/bahner/go-ma v0.4.0 + github.com/bahner/go-ma v0.4.1-0.20240206135113-165e47a9b140 github.com/fxamacker/cbor/v2 v2.5.0 github.com/gdamore/tcell/v2 v2.7.0 github.com/libp2p/go-libp2p v0.32.2 @@ -18,6 +18,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.18.2 + gopkg.in/yaml.v2 v2.4.0 ) require ( diff --git a/go.sum b/go.sum index b881336..e8c8478 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/alibabacloud-go/tea-utils v1.3.5/go.mod h1:EI/o33aBfj3hETm4RLiAxF/ThQ github.com/aliyun/credentials-go v1.1.0/go.mod h1:ZXrrxv386Mj6z8NpihLKpexQE550m7j3LlyCvYub9aE= github.com/aliyun/credentials-go v1.1.2/go.mod h1:ozcZaMR5kLM7pwtCMEpVmQ242suV6qTJya2bDq4X1Tw= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/bahner/go-ma v0.4.0 h1:JKHdjwzFwHakwe521ozJhLelCUgTqcuKtXAMnM2n7C0= -github.com/bahner/go-ma v0.4.0/go.mod h1:WM8wAs1tYjIuQLnM5kGkOo177N0dRQ4bXO4p/t63w+E= +github.com/bahner/go-ma v0.4.1-0.20240206135113-165e47a9b140 h1:SUosauh4xglni948TM2j+FJd0ze8sJ1uXpiqT91luLk= +github.com/bahner/go-ma v0.4.1-0.20240206135113-165e47a9b140/go.mod h1:WM8wAs1tYjIuQLnM5kGkOo177N0dRQ4bXO4p/t63w+E= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=