From a40313ecd9861820886ee79ac4bc9638c82143f2 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:42:30 -0800 Subject: [PATCH 01/14] implement shell plugin for authenticating with MotherDuck when using duckdb --- plugins/motherduck/access_token.go | 40 ++++++++++++++++++ plugins/motherduck/access_token_test.go | 55 +++++++++++++++++++++++++ plugins/motherduck/duckdb.go | 24 +++++++++++ plugins/motherduck/plugin.go | 22 ++++++++++ 4 files changed, 141 insertions(+) create mode 100644 plugins/motherduck/access_token.go create mode 100644 plugins/motherduck/access_token_test.go create mode 100644 plugins/motherduck/duckdb.go create mode 100644 plugins/motherduck/plugin.go diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go new file mode 100644 index 000000000..5dfcd29e2 --- /dev/null +++ b/plugins/motherduck/access_token.go @@ -0,0 +1,40 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/importer" + "github.com/1Password/shell-plugins/sdk/provision" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func AccessToken() schema.CredentialType { + return schema.CredentialType{ + Name: credname.AccessToken, + DocsURL: sdk.URL("https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#authentication-using-an-access-token"), + ManagementURL: sdk.URL("https://app.motherduck.com/settings/general"), + Fields: []schema.CredentialField{ + { + Name: fieldname.Token, + MarkdownDescription: "Token used to authenticate to MotherDuck.", + Secret: true, + Composition: &schema.ValueComposition{ + Length: 405, + Charset: schema.Charset{ + Uppercase: true, + Lowercase: true, + Digits: true, + }, + }, + }, + }, + DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), + Importer: importer.TryAll( + importer.TryEnvVarPair(defaultEnvVarMapping), + )} +} + +var defaultEnvVarMapping = map[string]sdk.FieldName{ + "motherduck_token": fieldname.Token, +} diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go new file mode 100644 index 000000000..51c3248a6 --- /dev/null +++ b/plugins/motherduck/access_token_test.go @@ -0,0 +1,55 @@ +package motherduck + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/plugintest" + "github.com/1Password/shell-plugins/sdk/schema/fieldname" +) + +func TestAccessTokenProvisioner(t *testing.T) { + plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ + "default": { + ItemFields: map[sdk.FieldName]string{ // TODO: Check if this is correct + fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + ExpectedOutput: sdk.ProvisionOutput{ + Environment: map[string]string{ + "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + }, + }, + }) +} + +func TestAccessTokenImporter(t *testing.T) { + plugintest.TestImporter(t, AccessToken().Importer, map[string]plugintest.ImportCase{ + "environment": { + Environment: map[string]string{ // TODO: Check if this is correct + "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + }, + }, + }, + // TODO: If you implemented a config file importer, add a test file example in motherduck/test-fixtures + // and fill the necessary details in the test template below. + "config file": { + Files: map[string]string{ + // "~/path/to/config.yml": plugintest.LoadFixture(t, "config.yml"), + }, + ExpectedCandidates: []sdk.ImportCandidate{ + // { + // Fields: map[sdk.FieldName]string{ + // fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + // }, + // }, + }, + }, + }) +} diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go new file mode 100644 index 000000000..cd79b0b5e --- /dev/null +++ b/plugins/motherduck/duckdb.go @@ -0,0 +1,24 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/needsauth" + "github.com/1Password/shell-plugins/sdk/schema" + "github.com/1Password/shell-plugins/sdk/schema/credname" +) + +func DuckDBCLI() schema.Executable { + return schema.Executable{ + Name: "DuckDB CLI", + Runs: []string{"duckdb"}, + DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), + NeedsAuth: needsauth.IfAll( + needsauth.NotForHelpOrVersion(), + ), + Uses: []schema.CredentialUsage{ + { + Name: credname.AccessToken, + }, + }, + } +} diff --git a/plugins/motherduck/plugin.go b/plugins/motherduck/plugin.go new file mode 100644 index 000000000..08673d2aa --- /dev/null +++ b/plugins/motherduck/plugin.go @@ -0,0 +1,22 @@ +package motherduck + +import ( + "github.com/1Password/shell-plugins/sdk" + "github.com/1Password/shell-plugins/sdk/schema" +) + +func New() schema.Plugin { + return schema.Plugin{ + Name: "motherduck", + Platform: schema.PlatformInfo{ + Name: "MotherDuck", + Homepage: sdk.URL("https://motherduck.com"), // TODO: Check if this is correct + }, + Credentials: []schema.CredentialType{ + AccessToken(), + }, + Executables: []schema.Executable{ + DuckDBCLI(), + }, + } +} From f1ce2381aa284261494e1ca4e897783bf86f481b Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:05:55 -0800 Subject: [PATCH 02/14] Don't use plugin if any of the args contain motherduck_token= --- plugins/motherduck/duckdb.go | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index cd79b0b5e..0e2a464e8 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -1,12 +1,46 @@ package motherduck import ( + "strings" + "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/needsauth" "github.com/1Password/shell-plugins/sdk/schema" "github.com/1Password/shell-plugins/sdk/schema/credname" ) +func NotWhenAnyArgsContain(argsSequence ...string) sdk.NeedsAuthentication { + return func(in sdk.NeedsAuthenticationInput) bool { + if len(argsSequence) == 0 { + return true + } + + if len(argsSequence) > len(in.CommandArgs) { + return true + } + + for i := range in.CommandArgs { + if i+len(argsSequence) > len(in.CommandArgs) { + return true + } + + matches := true + for i, argsToCompare := range in.CommandArgs[i : i+len(argsSequence)] { + if !strings.Contains(argsToCompare, argsSequence[i]) { + matches = false + } + } + + // If the argsToSkip are found in the command-line args, return that the command + // does not not require authentication + if matches { + return false + } + } + return true + } +} + func DuckDBCLI() schema.Executable { return schema.Executable{ Name: "DuckDB CLI", @@ -14,6 +48,7 @@ func DuckDBCLI() schema.Executable { DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), + NotWhenAnyArgsContain("motherduck_token="), ), Uses: []schema.CredentialUsage{ { From e08177cde68451a1a6471cb0815bb986f092fe73 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Thu, 23 Jan 2025 21:06:22 -0800 Subject: [PATCH 03/14] remove test for config file --- plugins/motherduck/access_token_test.go | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go index 51c3248a6..1c2d94889 100644 --- a/plugins/motherduck/access_token_test.go +++ b/plugins/motherduck/access_token_test.go @@ -2,12 +2,12 @@ package motherduck import ( "testing" - + "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/plugintest" "github.com/1Password/shell-plugins/sdk/schema/fieldname" ) - + func TestAccessTokenProvisioner(t *testing.T) { plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ "default": { @@ -37,19 +37,5 @@ func TestAccessTokenImporter(t *testing.T) { }, }, }, - // TODO: If you implemented a config file importer, add a test file example in motherduck/test-fixtures - // and fill the necessary details in the test template below. - "config file": { - Files: map[string]string{ - // "~/path/to/config.yml": plugintest.LoadFixture(t, "config.yml"), - }, - ExpectedCandidates: []sdk.ImportCandidate{ - // { - // Fields: map[sdk.FieldName]string{ - // fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", - // }, - // }, - }, - }, }) } From 00e7531ef0bb96a5d680be4d251dff3ce53769ff Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:23:49 -0700 Subject: [PATCH 04/14] - update management url - length is not fixed so remove - token can include . and _ --- plugins/motherduck/access_token.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go index 5dfcd29e2..234f15e3a 100644 --- a/plugins/motherduck/access_token.go +++ b/plugins/motherduck/access_token.go @@ -13,18 +13,18 @@ func AccessToken() schema.CredentialType { return schema.CredentialType{ Name: credname.AccessToken, DocsURL: sdk.URL("https://motherduck.com/docs/key-tasks/authenticating-and-connecting-to-motherduck/authenticating-to-motherduck/#authentication-using-an-access-token"), - ManagementURL: sdk.URL("https://app.motherduck.com/settings/general"), + ManagementURL: sdk.URL("https://app.motherduck.com/settings/tokens"), Fields: []schema.CredentialField{ { Name: fieldname.Token, MarkdownDescription: "Token used to authenticate to MotherDuck.", Secret: true, Composition: &schema.ValueComposition{ - Length: 405, Charset: schema.Charset{ Uppercase: true, Lowercase: true, Digits: true, + Specific: []rune{'.', '_'}, }, }, }, From 3cf454404f5deeb7df4e1b357aaecb66c51224a7 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:25:36 -0700 Subject: [PATCH 05/14] replace NotWhenAnyArgsContain with helper function specific to motherduck. Defer to environment variable or provided token value if either is set. --- plugins/motherduck/duckdb.go | 55 ++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 31 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index 0e2a464e8..85618fa11 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -1,6 +1,7 @@ package motherduck import ( + "os" "strings" "github.com/1Password/shell-plugins/sdk" @@ -9,36 +10,28 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/credname" ) -func NotWhenAnyArgsContain(argsSequence ...string) sdk.NeedsAuthentication { - return func(in sdk.NeedsAuthenticationInput) bool { - if len(argsSequence) == 0 { - return true - } - - if len(argsSequence) > len(in.CommandArgs) { - return true - } - - for i := range in.CommandArgs { - if i+len(argsSequence) > len(in.CommandArgs) { - return true - } - - matches := true - for i, argsToCompare := range in.CommandArgs[i : i+len(argsSequence)] { - if !strings.Contains(argsToCompare, argsSequence[i]) { - matches = false - } - } - - // If the argsToSkip are found in the command-line args, return that the command - // does not not require authentication - if matches { - return false - } - } - return true - } +// The plugin is only invoked if: +// - environment variable motherduck_token is not set +// - connection string contains 'md:' and does not contain 'motherduck_token=' +func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { + return func(in sdk.NeedsAuthenticationInput) bool { + // If environment variables are already set, we don't need to authenticate + if envValue := os.Getenv("motherduck_token"); envValue != "" { + return false + } + + // Otherwise, check if the command uses MotherDuck + if len(in.CommandArgs) == 0 { + return false + } + + for _, arg := range in.CommandArgs { + if strings.Contains(arg, "md:") && !strings.Contains(arg, "motherduck_token=") { + return true + } + } + return false + } } func DuckDBCLI() schema.Executable { @@ -48,7 +41,7 @@ func DuckDBCLI() schema.Executable { DocsURL: sdk.URL("https://duckdb.org/docs/api/cli/overview"), NeedsAuth: needsauth.IfAll( needsauth.NotForHelpOrVersion(), - NotWhenAnyArgsContain("motherduck_token="), + ForMotherDuckButTokenNotSet(), ), Uses: []schema.CredentialUsage{ { From 14818e7538077291d7d41e4c432fa9932fffc545 Mon Sep 17 00:00:00 2001 From: Michael Sterling <1461273+sterlinm@users.noreply.github.com> Date: Fri, 20 Jun 2025 00:25:42 -0700 Subject: [PATCH 06/14] remove todo --- plugins/motherduck/plugin.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/motherduck/plugin.go b/plugins/motherduck/plugin.go index 08673d2aa..8de964c66 100644 --- a/plugins/motherduck/plugin.go +++ b/plugins/motherduck/plugin.go @@ -10,7 +10,7 @@ func New() schema.Plugin { Name: "motherduck", Platform: schema.PlatformInfo{ Name: "MotherDuck", - Homepage: sdk.URL("https://motherduck.com"), // TODO: Check if this is correct + Homepage: sdk.URL("https://motherduck.com"), }, Credentials: []schema.CredentialType{ AccessToken(), From 8e5dddfe082c29536692d7d2b5cfcdc761101409 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:16:09 -0400 Subject: [PATCH 07/14] style(motherduck): gofmt duckdb.go Replace space indentation with tabs in ForMotherDuckButTokenNotSet so golangci-lint's gofmt check passes. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/duckdb.go | 38 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index 85618fa11..ac90cc0b8 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -11,27 +11,27 @@ import ( ) // The plugin is only invoked if: -// - environment variable motherduck_token is not set -// - connection string contains 'md:' and does not contain 'motherduck_token=' +// - environment variable motherduck_token is not set +// - connection string contains 'md:' and does not contain 'motherduck_token=' func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { - return func(in sdk.NeedsAuthenticationInput) bool { - // If environment variables are already set, we don't need to authenticate - if envValue := os.Getenv("motherduck_token"); envValue != "" { - return false - } - - // Otherwise, check if the command uses MotherDuck - if len(in.CommandArgs) == 0 { - return false - } + return func(in sdk.NeedsAuthenticationInput) bool { + // If environment variables are already set, we don't need to authenticate + if envValue := os.Getenv("motherduck_token"); envValue != "" { + return false + } - for _, arg := range in.CommandArgs { - if strings.Contains(arg, "md:") && !strings.Contains(arg, "motherduck_token=") { - return true - } - } - return false - } + // Otherwise, check if the command uses MotherDuck + if len(in.CommandArgs) == 0 { + return false + } + + for _, arg := range in.CommandArgs { + if strings.Contains(arg, "md:") && !strings.Contains(arg, "motherduck_token=") { + return true + } + } + return false + } } func DuckDBCLI() schema.Executable { From cf7d5a4558dc5facf2cd909d18cf1e4ec53c9b90 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:16:10 -0400 Subject: [PATCH 08/14] test(motherduck): use motherduck_token in access token tests The provisioner and importer use the lowercase motherduck_token env var, which the MotherDuck extension gives precedence over MOTHERDUCK_TOKEN. Align the test expectations with that and drop leftover TODO comments. --- plugins/motherduck/access_token_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go index 1c2d94889..2d1ed82b7 100644 --- a/plugins/motherduck/access_token_test.go +++ b/plugins/motherduck/access_token_test.go @@ -11,12 +11,12 @@ import ( func TestAccessTokenProvisioner(t *testing.T) { plugintest.TestProvisioner(t, AccessToken().DefaultProvisioner, map[string]plugintest.ProvisionCase{ "default": { - ItemFields: map[sdk.FieldName]string{ // TODO: Check if this is correct + ItemFields: map[sdk.FieldName]string{ fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", }, ExpectedOutput: sdk.ProvisionOutput{ Environment: map[string]string{ - "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + "motherduck_token": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", }, }, }, @@ -26,8 +26,8 @@ func TestAccessTokenProvisioner(t *testing.T) { func TestAccessTokenImporter(t *testing.T) { plugintest.TestImporter(t, AccessToken().Importer, map[string]plugintest.ImportCase{ "environment": { - Environment: map[string]string{ // TODO: Check if this is correct - "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + Environment: map[string]string{ + "motherduck_token": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", }, ExpectedCandidates: []sdk.ImportCandidate{ { From 088cf3ca267b2e80349a630b82a7a5d2fc2412fa Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:28:35 -0400 Subject: [PATCH 09/14] test(motherduck): cover when the DuckDB CLI needs authentication Add NeedsAuth cases for local databases, MotherDuck connection strings, ATTACH from a command, and a token supplied via the connection string or the motherduck_token env var. Token env vars are cleared so a developer's own shell doesn't affect the results. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/duckdb_test.go | 64 +++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 plugins/motherduck/duckdb_test.go diff --git a/plugins/motherduck/duckdb_test.go b/plugins/motherduck/duckdb_test.go new file mode 100644 index 000000000..3d9635c68 --- /dev/null +++ b/plugins/motherduck/duckdb_test.go @@ -0,0 +1,64 @@ +package motherduck + +import ( + "testing" + + "github.com/1Password/shell-plugins/sdk/plugintest" +) + +// unsetTokenEnvVars keeps tokens from the developer's own shell from leaking into the tests. +func unsetTokenEnvVars(t *testing.T) { + t.Setenv("motherduck_token", "") + t.Setenv("MOTHERDUCK_TOKEN", "") +} + +func TestDuckDBCLINeedsAuth(t *testing.T) { + unsetTokenEnvVars(t) + + plugintest.TestNeedsAuth(t, DuckDBCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{ + "no args opens an in-memory database": { + Args: []string{}, + ExpectedNeedsAuth: false, + }, + "local database file": { + Args: []string{"local.ddb"}, + ExpectedNeedsAuth: false, + }, + "local database file with a command": { + Args: []string{"local.ddb", "-c", "select 1"}, + ExpectedNeedsAuth: false, + }, + "default MotherDuck database": { + Args: []string{"md:"}, + ExpectedNeedsAuth: true, + }, + "named MotherDuck database": { + Args: []string{"md:my_db"}, + ExpectedNeedsAuth: true, + }, + "MotherDuck database with a command": { + Args: []string{"md:my_db", "-c", "select 1"}, + ExpectedNeedsAuth: true, + }, + "MotherDuck attached from a command": { + Args: []string{"-c", "ATTACH 'md:'"}, + ExpectedNeedsAuth: true, + }, + "token passed in the connection string": { + Args: []string{"md:my_db?motherduck_token=abc"}, + ExpectedNeedsAuth: false, + }, + }) +} + +func TestDuckDBCLINeedsAuthWithTokenEnvVar(t *testing.T) { + unsetTokenEnvVars(t) + t.Setenv("motherduck_token", "abc") + + plugintest.TestNeedsAuth(t, DuckDBCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{ + "MotherDuck database": { + Args: []string{"md:my_db"}, + ExpectedNeedsAuth: false, + }, + }) +} From 678e4fe8e1e2b476e6e14926f76fe1b99b7bfa58 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:29:21 -0400 Subject: [PATCH 10/14] fix(motherduck): don't authenticate when MOTHERDUCK_TOKEN is set The MotherDuck extension reads its token from either motherduck_token or MOTHERDUCK_TOKEN. The plugin only deferred to motherduck_token, so with just MOTHERDUCK_TOKEN set it still prompted and injected motherduck_token, which takes precedence and silently replaced the user's token. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/access_token.go | 4 ++++ plugins/motherduck/duckdb.go | 10 ++++++---- plugins/motherduck/duckdb_test.go | 20 ++++++++++++-------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go index 234f15e3a..76d1d9342 100644 --- a/plugins/motherduck/access_token.go +++ b/plugins/motherduck/access_token.go @@ -38,3 +38,7 @@ func AccessToken() schema.CredentialType { var defaultEnvVarMapping = map[string]sdk.FieldName{ "motherduck_token": fieldname.Token, } + +// tokenEnvVars are the env vars the MotherDuck extension reads a token from. +// When both are set, motherduck_token takes precedence. +var tokenEnvVars = []string{"motherduck_token", "MOTHERDUCK_TOKEN"} diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index ac90cc0b8..22a4c9137 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -11,13 +11,15 @@ import ( ) // The plugin is only invoked if: -// - environment variable motherduck_token is not set +// - neither the motherduck_token nor the MOTHERDUCK_TOKEN environment variable is set // - connection string contains 'md:' and does not contain 'motherduck_token=' func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { return func(in sdk.NeedsAuthenticationInput) bool { - // If environment variables are already set, we don't need to authenticate - if envValue := os.Getenv("motherduck_token"); envValue != "" { - return false + // If a token is already set in the environment, we don't need to authenticate + for _, envVar := range tokenEnvVars { + if os.Getenv(envVar) != "" { + return false + } } // Otherwise, check if the command uses MotherDuck diff --git a/plugins/motherduck/duckdb_test.go b/plugins/motherduck/duckdb_test.go index 3d9635c68..700e8ff72 100644 --- a/plugins/motherduck/duckdb_test.go +++ b/plugins/motherduck/duckdb_test.go @@ -52,13 +52,17 @@ func TestDuckDBCLINeedsAuth(t *testing.T) { } func TestDuckDBCLINeedsAuthWithTokenEnvVar(t *testing.T) { - unsetTokenEnvVars(t) - t.Setenv("motherduck_token", "abc") + for _, envVar := range []string{"motherduck_token", "MOTHERDUCK_TOKEN"} { + t.Run(envVar, func(t *testing.T) { + unsetTokenEnvVars(t) + t.Setenv(envVar, "abc") - plugintest.TestNeedsAuth(t, DuckDBCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{ - "MotherDuck database": { - Args: []string{"md:my_db"}, - ExpectedNeedsAuth: false, - }, - }) + plugintest.TestNeedsAuth(t, DuckDBCLI().NeedsAuth, map[string]plugintest.NeedsAuthCase{ + "MotherDuck database": { + Args: []string{"md:my_db"}, + ExpectedNeedsAuth: false, + }, + }) + }) + } } From bd2a0a61821f8270af742ee7157c396f8b889cb1 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:29:45 -0400 Subject: [PATCH 11/14] feat(motherduck): recognize motherduck: and uppercase connection prefixes DuckDB connects to MotherDuck for 'motherduck:' as well as 'md:', and matches the prefix case-insensitively ('MD:' works too). Those connections previously skipped the plugin and fell back to MotherDuck's browser login. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/duckdb.go | 9 +++++++-- plugins/motherduck/duckdb_test.go | 12 ++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index 22a4c9137..ea3562ac8 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -2,6 +2,7 @@ package motherduck import ( "os" + "regexp" "strings" "github.com/1Password/shell-plugins/sdk" @@ -10,9 +11,13 @@ import ( "github.com/1Password/shell-plugins/sdk/schema/credname" ) +// motherDuckConnection matches MotherDuck connection strings like 'md:', 'md:my_db' or 'motherduck:my_db'. +// DuckDB treats the prefix case-insensitively. +var motherDuckConnection = regexp.MustCompile(`(?i)(md|motherduck):`) + // The plugin is only invoked if: // - neither the motherduck_token nor the MOTHERDUCK_TOKEN environment variable is set -// - connection string contains 'md:' and does not contain 'motherduck_token=' +// - an argument contains an 'md:' or 'motherduck:' connection string that does not contain 'motherduck_token=' func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { return func(in sdk.NeedsAuthenticationInput) bool { // If a token is already set in the environment, we don't need to authenticate @@ -28,7 +33,7 @@ func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { } for _, arg := range in.CommandArgs { - if strings.Contains(arg, "md:") && !strings.Contains(arg, "motherduck_token=") { + if motherDuckConnection.MatchString(arg) && !strings.Contains(arg, "motherduck_token=") { return true } } diff --git a/plugins/motherduck/duckdb_test.go b/plugins/motherduck/duckdb_test.go index 700e8ff72..9c4a30274 100644 --- a/plugins/motherduck/duckdb_test.go +++ b/plugins/motherduck/duckdb_test.go @@ -36,6 +36,14 @@ func TestDuckDBCLINeedsAuth(t *testing.T) { Args: []string{"md:my_db"}, ExpectedNeedsAuth: true, }, + "motherduck: prefix": { + Args: []string{"motherduck:my_db"}, + ExpectedNeedsAuth: true, + }, + "uppercase prefix": { + Args: []string{"MD:my_db"}, + ExpectedNeedsAuth: true, + }, "MotherDuck database with a command": { Args: []string{"md:my_db", "-c", "select 1"}, ExpectedNeedsAuth: true, @@ -44,6 +52,10 @@ func TestDuckDBCLINeedsAuth(t *testing.T) { Args: []string{"-c", "ATTACH 'md:'"}, ExpectedNeedsAuth: true, }, + "MotherDuck attached with the motherduck: prefix": { + Args: []string{"-c", "ATTACH 'motherduck:my_db'"}, + ExpectedNeedsAuth: true, + }, "token passed in the connection string": { Args: []string{"md:my_db?motherduck_token=abc"}, ExpectedNeedsAuth: false, From 1f2a1604e296ec885e911c2d0542e025add75aa9 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:30:01 -0400 Subject: [PATCH 12/14] fix(motherduck): don't treat md: inside other words as a connection A plain substring match on "md:" also fired on text like 'cmd:' inside a query passed with -c, prompting for a token DuckDB wouldn't use. Require the prefix to not follow a word character. Also drop the empty-args check, which the loop already covers. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/duckdb.go | 9 +++------ plugins/motherduck/duckdb_test.go | 4 ++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index ea3562ac8..5880f1d2c 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -12,8 +12,9 @@ import ( ) // motherDuckConnection matches MotherDuck connection strings like 'md:', 'md:my_db' or 'motherduck:my_db'. -// DuckDB treats the prefix case-insensitively. -var motherDuckConnection = regexp.MustCompile(`(?i)(md|motherduck):`) +// DuckDB treats the prefix case-insensitively. The prefix must not follow a word character, +// so text like 'cmd:' inside a query doesn't count. +var motherDuckConnection = regexp.MustCompile(`(?i)(^|[^[:alnum:]_])(md|motherduck):`) // The plugin is only invoked if: // - neither the motherduck_token nor the MOTHERDUCK_TOKEN environment variable is set @@ -28,10 +29,6 @@ func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { } // Otherwise, check if the command uses MotherDuck - if len(in.CommandArgs) == 0 { - return false - } - for _, arg := range in.CommandArgs { if motherDuckConnection.MatchString(arg) && !strings.Contains(arg, "motherduck_token=") { return true diff --git a/plugins/motherduck/duckdb_test.go b/plugins/motherduck/duckdb_test.go index 9c4a30274..67014d58b 100644 --- a/plugins/motherduck/duckdb_test.go +++ b/plugins/motherduck/duckdb_test.go @@ -28,6 +28,10 @@ func TestDuckDBCLINeedsAuth(t *testing.T) { Args: []string{"local.ddb", "-c", "select 1"}, ExpectedNeedsAuth: false, }, + "md: inside another word in a command": { + Args: []string{"local.ddb", "-c", "select 'cmd:ls'"}, + ExpectedNeedsAuth: false, + }, "default MotherDuck database": { Args: []string{"md:"}, ExpectedNeedsAuth: true, From e25646d6377bdfdb5f9236539e21a2c982765566 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:30:25 -0400 Subject: [PATCH 13/14] fix(motherduck): don't authenticate when the connection string has token= DuckDB accepts 'token=' as an alias for 'motherduck_token=' in a MotherDuck connection string, and that token wins over any env var. Skip the plugin for either parameter so users aren't prompted for a token that won't be used. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/duckdb.go | 9 ++++++--- plugins/motherduck/duckdb_test.go | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/plugins/motherduck/duckdb.go b/plugins/motherduck/duckdb.go index 5880f1d2c..34a5c8322 100644 --- a/plugins/motherduck/duckdb.go +++ b/plugins/motherduck/duckdb.go @@ -3,7 +3,6 @@ package motherduck import ( "os" "regexp" - "strings" "github.com/1Password/shell-plugins/sdk" "github.com/1Password/shell-plugins/sdk/needsauth" @@ -16,9 +15,13 @@ import ( // so text like 'cmd:' inside a query doesn't count. var motherDuckConnection = regexp.MustCompile(`(?i)(^|[^[:alnum:]_])(md|motherduck):`) +// connectionStringToken matches a token passed as a connection string parameter, +// e.g. 'md:my_db?motherduck_token=...' or its 'token=' alias. DuckDB uses it over any env var. +var connectionStringToken = regexp.MustCompile(`[?&](motherduck_)?token=`) + // The plugin is only invoked if: // - neither the motherduck_token nor the MOTHERDUCK_TOKEN environment variable is set -// - an argument contains an 'md:' or 'motherduck:' connection string that does not contain 'motherduck_token=' +// - an argument contains an 'md:' or 'motherduck:' connection string that does not include a token func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { return func(in sdk.NeedsAuthenticationInput) bool { // If a token is already set in the environment, we don't need to authenticate @@ -30,7 +33,7 @@ func ForMotherDuckButTokenNotSet() sdk.NeedsAuthentication { // Otherwise, check if the command uses MotherDuck for _, arg := range in.CommandArgs { - if motherDuckConnection.MatchString(arg) && !strings.Contains(arg, "motherduck_token=") { + if motherDuckConnection.MatchString(arg) && !connectionStringToken.MatchString(arg) { return true } } diff --git a/plugins/motherduck/duckdb_test.go b/plugins/motherduck/duckdb_test.go index 67014d58b..5963a08ee 100644 --- a/plugins/motherduck/duckdb_test.go +++ b/plugins/motherduck/duckdb_test.go @@ -64,6 +64,14 @@ func TestDuckDBCLINeedsAuth(t *testing.T) { Args: []string{"md:my_db?motherduck_token=abc"}, ExpectedNeedsAuth: false, }, + "token alias passed in the connection string": { + Args: []string{"md:my_db?token=abc"}, + ExpectedNeedsAuth: false, + }, + "token passed after another connection string parameter": { + Args: []string{"md:my_db?attach_mode=single&motherduck_token=abc"}, + ExpectedNeedsAuth: false, + }, }) } From 22f1196cd3b61387f7a9f07905823c082cb5bd20 Mon Sep 17 00:00:00 2001 From: Riyad Khan Date: Thu, 8 Oct 2026 14:30:52 -0400 Subject: [PATCH 14/14] feat(motherduck): import the token from MOTHERDUCK_TOKEN too The importer only looked at motherduck_token, so a token exported as MOTHERDUCK_TOKEN, which the MotherDuck extension also reads, wasn't offered during op plugin init. Co-Authored-By: Claude Opus 5.5 --- plugins/motherduck/access_token.go | 2 +- plugins/motherduck/access_token_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/plugins/motherduck/access_token.go b/plugins/motherduck/access_token.go index 76d1d9342..d02f35dd4 100644 --- a/plugins/motherduck/access_token.go +++ b/plugins/motherduck/access_token.go @@ -31,7 +31,7 @@ func AccessToken() schema.CredentialType { }, DefaultProvisioner: provision.EnvVars(defaultEnvVarMapping), Importer: importer.TryAll( - importer.TryEnvVarPair(defaultEnvVarMapping), + importer.TryAllEnvVars(fieldname.Token, tokenEnvVars...), )} } diff --git a/plugins/motherduck/access_token_test.go b/plugins/motherduck/access_token_test.go index 2d1ed82b7..52c87635b 100644 --- a/plugins/motherduck/access_token_test.go +++ b/plugins/motherduck/access_token_test.go @@ -28,6 +28,20 @@ func TestAccessTokenImporter(t *testing.T) { "environment": { Environment: map[string]string{ "motherduck_token": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + "MOTHERDUCK_TOKEN": "", + }, + ExpectedCandidates: []sdk.ImportCandidate{ + { + Fields: map[sdk.FieldName]string{ + fieldname.Token: "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", + }, + }, + }, + }, + "uppercase environment variable": { + Environment: map[string]string{ + "motherduck_token": "", + "MOTHERDUCK_TOKEN": "TERAkHVPg65C6UGDw42llLlgPtZhBbafxnpqs74fjyuKnDpSt7TZNODw3catnivaruR09REDcNIwystkLMlRw5foxRjvytBFmkk0t0x9iHqY0MBY40Ltbcdw8fvt3OzsCgxmbh89v0XIWrRiwCfALA1dbqWDLaatAZWOLQhJmYcggQR6YBVoKM9H7XBrBjDtP7YJOoU2Z7rc7KWgTTqS9vyCtLx7GDSBitWQLvUYuWzvgh94qk1Wt16oua34jzDtosd59ahNlvA1vEqPtkYqC5mNbDbWqcunwelka4tI4uuEfyojeXowBzkv6izjT48J3usTPIIqTFMYgJnMwUtV6n8UgeuLumEsKd86HVLywapqO37zfNrlrVLzjHSv0rGA2NjDgBAueK2clqEXAMPLE", }, ExpectedCandidates: []sdk.ImportCandidate{ {