From 1659eb193d452ae00c7236e6aeb9c153acf40095 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 09:23:15 -0800 Subject: [PATCH 1/3] refactor: switch to fork of go-keyring library - Replace internal keyring wrapper with direct use of github.com/zalando/go-keyring - Use replace directive to point to github.com/kitproj/go-keyring fork - Update tests to clear keyring before testing missing token scenarios --- go.mod | 5 +- go.sum | 8 +- internal/keyring/keyring.go | 22 ---- internal/keyring/keyring_darwin.go | 24 ----- internal/keyring/keyring_linux.go | 96 ----------------- internal/keyring/keyring_test.go | 153 ---------------------------- internal/keyring/keyring_windows.go | 24 ----- main.go | 2 +- main_test.go | 7 +- mcp_test.go | 7 +- 10 files changed, 18 insertions(+), 330 deletions(-) delete mode 100644 internal/keyring/keyring.go delete mode 100644 internal/keyring/keyring_darwin.go delete mode 100644 internal/keyring/keyring_linux.go delete mode 100644 internal/keyring/keyring_test.go delete mode 100644 internal/keyring/keyring_windows.go diff --git a/go.mod b/go.mod index 386d706..ee762ca 100644 --- a/go.mod +++ b/go.mod @@ -2,15 +2,16 @@ module github.com/kitproj/slack-cli go 1.24.4 +replace github.com/zalando/go-keyring => github.com/kitproj/go-keyring v0.2.8 + require ( github.com/mark3labs/mcp-go v0.42.0 github.com/slack-go/slack v0.17.3 - github.com/zalando/go-keyring v0.2.6 + github.com/zalando/go-keyring v0.2.8 golang.org/x/term v0.36.0 ) require ( - al.essio.dev/pkg/shellescape v1.5.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/danieljoos/wincred v1.2.2 // indirect diff --git a/go.sum b/go.sum index 2508c9e..c39aabc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= -al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -16,8 +14,6 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -25,6 +21,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kitproj/go-keyring v0.2.8 h1:joZWEivgzv6EaPAiCbgSHvnjmFuVR9AHtYCh4GmBBxQ= +github.com/kitproj/go-keyring v0.2.8/go.mod h1:yhtJhnoQt44WWzPtoc44uANw82MILytMeDc01iKC8cM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -49,8 +47,6 @@ github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/ github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= -github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q= diff --git a/internal/keyring/keyring.go b/internal/keyring/keyring.go deleted file mode 100644 index 9306010..0000000 --- a/internal/keyring/keyring.go +++ /dev/null @@ -1,22 +0,0 @@ -package keyring - -// Provider defines the interface for token storage -type Provider interface { - // Set stores a token for the given service and user - Set(service, user, token string) error - // Get retrieves a token for the given service and user - Get(service, user string) (string, error) -} - -// provider is the platform-specific implementation -var provider Provider - -// Set stores a token using the platform-specific provider -func Set(service, user, token string) error { - return provider.Set(service, user, token) -} - -// Get retrieves a token using the platform-specific provider -func Get(service, user string) (string, error) { - return provider.Get(service, user) -} diff --git a/internal/keyring/keyring_darwin.go b/internal/keyring/keyring_darwin.go deleted file mode 100644 index 9c1e751..0000000 --- a/internal/keyring/keyring_darwin.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build darwin - -package keyring - -import ( - "github.com/zalando/go-keyring" -) - -func init() { - provider = &systemKeyringProvider{} -} - -// systemKeyringProvider implements token storage using the system keyring -type systemKeyringProvider struct{} - -// Set stores a token in the system keyring -func (s *systemKeyringProvider) Set(service, user, token string) error { - return keyring.Set(service, user, token) -} - -// Get retrieves a token from the system keyring -func (s *systemKeyringProvider) Get(service, user string) (string, error) { - return keyring.Get(service, user) -} diff --git a/internal/keyring/keyring_linux.go b/internal/keyring/keyring_linux.go deleted file mode 100644 index 3bdfe20..0000000 --- a/internal/keyring/keyring_linux.go +++ /dev/null @@ -1,96 +0,0 @@ -//go:build linux - -package keyring - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" -) - -const tokenFile = "token" - -func init() { - provider = &fileProvider{} -} - -// fileProvider implements token storage using files -type fileProvider struct{} - -// Set stores a token in a file -func (f *fileProvider) Set(service, user, token string) error { - tokenPath, err := getTokenFilePath() - if err != nil { - return err - } - - // Create config directory if it doesn't exist - configDirPath := filepath.Dir(tokenPath) - if err := os.MkdirAll(configDirPath, 0700); err != nil { - return fmt.Errorf("failed to create config directory: %w", err) - } - - // Store as a simple key-value map - tokens := make(map[string]string) - - // Try to load existing tokens - if data, err := os.ReadFile(tokenPath); err == nil { - _ = json.Unmarshal(data, &tokens) - } - - // Add or update token for this user - tokens[user] = token - - // Save to file - data, err := json.MarshalIndent(tokens, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal tokens: %w", err) - } - - // Write with 0600 permissions (only owner can read/write) - if err := os.WriteFile(tokenPath, data, 0600); err != nil { - return fmt.Errorf("failed to write token file: %w", err) - } - - return nil -} - -// Get retrieves a token from a file -func (f *fileProvider) Get(service, user string) (string, error) { - tokenPath, err := getTokenFilePath() - if err != nil { - return "", err - } - - data, err := os.ReadFile(tokenPath) - if err != nil { - if os.IsNotExist(err) { - return "", fmt.Errorf("token not found") - } - return "", fmt.Errorf("failed to read token file: %w", err) - } - - tokens := make(map[string]string) - if err := json.Unmarshal(data, &tokens); err != nil { - return "", fmt.Errorf("failed to parse token file: %w", err) - } - - token, ok := tokens[user] - if !ok { - return "", fmt.Errorf("token not found for user: %s", user) - } - - return token, nil -} - -// getTokenFilePath returns the path to the token file -func getTokenFilePath() (string, error) { - configDirPath, err := os.UserConfigDir() - if err != nil { - return "", fmt.Errorf("failed to get config directory: %w", err) - } - - tokenPath := filepath.Join(configDirPath, "slack-cli", tokenFile) - return tokenPath, nil -} diff --git a/internal/keyring/keyring_test.go b/internal/keyring/keyring_test.go deleted file mode 100644 index 687d2fb..0000000 --- a/internal/keyring/keyring_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package keyring - -import ( - "os" - "path/filepath" - "testing" -) - -// TestSetGet tests basic set and get operations -func TestSetGet(t *testing.T) { - // Create a temporary directory for testing - tmpDir := t.TempDir() - - // Override the config directory - origConfigDir := os.Getenv("XDG_CONFIG_HOME") - os.Setenv("XDG_CONFIG_HOME", tmpDir) - defer func() { - if origConfigDir != "" { - os.Setenv("XDG_CONFIG_HOME", origConfigDir) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - testService := "test-service" - testUser := "test-user" - testToken := "test-token-12345" - - // Test Set - err := Set(testService, testUser, testToken) - if err != nil { - t.Fatalf("Failed to set token: %v", err) - } - - // Test Get - retrievedToken, err := Get(testService, testUser) - if err != nil { - t.Fatalf("Failed to get token: %v", err) - } - - if retrievedToken != testToken { - t.Errorf("Expected token %q, got %q", testToken, retrievedToken) - } -} - -// TestMultipleUsers tests storing tokens for multiple users -func TestMultipleUsers(t *testing.T) { - // Create a temporary directory for testing - tmpDir := t.TempDir() - - // Override the config directory - origConfigDir := os.Getenv("XDG_CONFIG_HOME") - os.Setenv("XDG_CONFIG_HOME", tmpDir) - defer func() { - if origConfigDir != "" { - os.Setenv("XDG_CONFIG_HOME", origConfigDir) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - testService := "test-service" - users := map[string]string{ - "user1": "token1", - "user2": "token2", - "user3": "token3", - } - - // Set all tokens - for user, token := range users { - err := Set(testService, user, token) - if err != nil { - t.Fatalf("Failed to set token for %s: %v", user, err) - } - } - - // Get and verify all tokens - for user, expectedToken := range users { - retrievedToken, err := Get(testService, user) - if err != nil { - t.Fatalf("Failed to get token for %s: %v", user, err) - } - if retrievedToken != expectedToken { - t.Errorf("For user %s, expected token %q, got %q", user, expectedToken, retrievedToken) - } - } -} - -// TestGetNotFound tests error handling when token doesn't exist -func TestGetNotFound(t *testing.T) { - // Create a temporary directory for testing - tmpDir := t.TempDir() - - // Override the config directory - origConfigDir := os.Getenv("XDG_CONFIG_HOME") - os.Setenv("XDG_CONFIG_HOME", tmpDir) - defer func() { - if origConfigDir != "" { - os.Setenv("XDG_CONFIG_HOME", origConfigDir) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - // Try to get from non-existent file - _, err := Get("test-service", "nonexistent-user") - if err == nil { - t.Error("Expected error when getting non-existent token, got nil") - } -} - -// TestFilePermissions tests that token file has correct permissions (Linux only) -func TestFilePermissions(t *testing.T) { - // Skip on non-Linux platforms since file implementation is Linux-specific - if provider == nil { - t.Skip("Skipping file permissions test on non-Linux platform") - } - - // Check if provider is fileProvider (Linux) - if _, ok := provider.(*fileProvider); !ok { - t.Skip("Skipping file permissions test on non-Linux platform") - } - - tmpDir := t.TempDir() - - origConfigDir := os.Getenv("XDG_CONFIG_HOME") - os.Setenv("XDG_CONFIG_HOME", tmpDir) - defer func() { - if origConfigDir != "" { - os.Setenv("XDG_CONFIG_HOME", origConfigDir) - } else { - os.Unsetenv("XDG_CONFIG_HOME") - } - }() - - // Set a token - err := Set("test-service", "test-user", "test-token") - if err != nil { - t.Fatalf("Failed to set token: %v", err) - } - - // Check file permissions - tokenPath := filepath.Join(tmpDir, "slack-cli", "token") - info, err := os.Stat(tokenPath) - if err != nil { - t.Fatalf("Failed to stat token file: %v", err) - } - - expectedPerm := os.FileMode(0600) - if info.Mode().Perm() != expectedPerm { - t.Errorf("Expected file permissions %v, got %v", expectedPerm, info.Mode().Perm()) - } -} diff --git a/internal/keyring/keyring_windows.go b/internal/keyring/keyring_windows.go deleted file mode 100644 index 5e3a410..0000000 --- a/internal/keyring/keyring_windows.go +++ /dev/null @@ -1,24 +0,0 @@ -//go:build windows - -package keyring - -import ( - "github.com/zalando/go-keyring" -) - -func init() { - provider = &systemKeyringProvider{} -} - -// systemKeyringProvider implements token storage using the system keyring -type systemKeyringProvider struct{} - -// Set stores a token in the system keyring -func (s *systemKeyringProvider) Set(service, user, token string) error { - return keyring.Set(service, user, token) -} - -// Get retrieves a token from the system keyring -func (s *systemKeyringProvider) Get(service, user string) (string, error) { - return keyring.Get(service, user) -} diff --git a/main.go b/main.go index 544334a..ce9be2a 100644 --- a/main.go +++ b/main.go @@ -10,8 +10,8 @@ import ( "strings" "syscall" - "github.com/kitproj/slack-cli/internal/keyring" "github.com/slack-go/slack" + "github.com/zalando/go-keyring" "golang.org/x/term" ) diff --git a/main_test.go b/main_test.go index 10bd09d..5454953 100644 --- a/main_test.go +++ b/main_test.go @@ -5,6 +5,8 @@ import ( "os" "strings" "testing" + + "github.com/zalando/go-keyring" ) func TestConfigure_EmptyToken(t *testing.T) { @@ -104,7 +106,7 @@ func TestRun_SendMessageMissingArgs(t *testing.T) { } func TestRun_SendMessageMissingToken(t *testing.T) { - // Ensure SLACK_TOKEN env var is not set + // Ensure SLACK_TOKEN env var is not set and keyring is cleared oldToken := os.Getenv("SLACK_TOKEN") os.Unsetenv("SLACK_TOKEN") defer func() { @@ -113,6 +115,9 @@ func TestRun_SendMessageMissingToken(t *testing.T) { } }() + // Clear keyring to ensure no token is stored + _ = keyring.Delete(keyringService, keyringUser) + ctx := context.Background() err := run(ctx, []string{"send-message", "C1234567890", "test message"}) diff --git a/mcp_test.go b/mcp_test.go index 261f1bb..afb5a46 100644 --- a/mcp_test.go +++ b/mcp_test.go @@ -5,6 +5,8 @@ import ( "os" "strings" "testing" + + "github.com/zalando/go-keyring" ) func TestRun_MCPServer(t *testing.T) { @@ -30,7 +32,7 @@ func TestRun_MCPServer(t *testing.T) { } func TestRun_MCPServerMissingToken(t *testing.T) { - // Unset SLACK_TOKEN env var + // Unset SLACK_TOKEN env var and clear keyring oldToken := os.Getenv("SLACK_TOKEN") os.Unsetenv("SLACK_TOKEN") defer func() { @@ -39,6 +41,9 @@ func TestRun_MCPServerMissingToken(t *testing.T) { } }() + // Clear keyring to ensure no token is stored + _ = keyring.Delete(keyringService, keyringUser) + ctx := context.Background() err := run(ctx, []string{"mcp-server"}) From 1de28d43f13229707899c05dca7e5d0473b77c44 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 09:27:28 -0800 Subject: [PATCH 2/3] chore: update go-keyring fork to v0.2.9 --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index ee762ca..4adafe1 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,12 @@ module github.com/kitproj/slack-cli go 1.24.4 -replace github.com/zalando/go-keyring => github.com/kitproj/go-keyring v0.2.8 +replace github.com/zalando/go-keyring => github.com/kitproj/go-keyring v0.2.9 require ( github.com/mark3labs/mcp-go v0.42.0 github.com/slack-go/slack v0.17.3 - github.com/zalando/go-keyring v0.2.8 + github.com/zalando/go-keyring v0.2.9 golang.org/x/term v0.36.0 ) diff --git a/go.sum b/go.sum index c39aabc..ae36295 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kitproj/go-keyring v0.2.8 h1:joZWEivgzv6EaPAiCbgSHvnjmFuVR9AHtYCh4GmBBxQ= -github.com/kitproj/go-keyring v0.2.8/go.mod h1:yhtJhnoQt44WWzPtoc44uANw82MILytMeDc01iKC8cM= +github.com/kitproj/go-keyring v0.2.9 h1:NNBpFfDcaPAWJBNFXhhuTJoUrRK5eWPOA2tPMymtZGY= +github.com/kitproj/go-keyring v0.2.9/go.mod h1:yhtJhnoQt44WWzPtoc44uANw82MILytMeDc01iKC8cM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= From fa05a4843d049e1c0e213c74c16e7c75faeba1d6 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Sat, 22 Nov 2025 09:30:59 -0800 Subject: [PATCH 3/3] chore: update go-keyring fork to v0.2.10 --- go.mod | 4 ++-- go.sum | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 4adafe1..3d815a6 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,12 @@ module github.com/kitproj/slack-cli go 1.24.4 -replace github.com/zalando/go-keyring => github.com/kitproj/go-keyring v0.2.9 +replace github.com/zalando/go-keyring => github.com/kitproj/go-keyring v0.2.10 require ( github.com/mark3labs/mcp-go v0.42.0 github.com/slack-go/slack v0.17.3 - github.com/zalando/go-keyring v0.2.9 + github.com/zalando/go-keyring v0.2.10 golang.org/x/term v0.36.0 ) diff --git a/go.sum b/go.sum index ae36295..b28c4d1 100644 --- a/go.sum +++ b/go.sum @@ -21,8 +21,8 @@ github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kitproj/go-keyring v0.2.9 h1:NNBpFfDcaPAWJBNFXhhuTJoUrRK5eWPOA2tPMymtZGY= -github.com/kitproj/go-keyring v0.2.9/go.mod h1:yhtJhnoQt44WWzPtoc44uANw82MILytMeDc01iKC8cM= +github.com/kitproj/go-keyring v0.2.10 h1:ZjwJOV8mIG8pYrWSGxtdGj62sYg6uAdMVa8kPbeMicQ= +github.com/kitproj/go-keyring v0.2.10/go.mod h1:yhtJhnoQt44WWzPtoc44uANw82MILytMeDc01iKC8cM= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=