Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 14 additions & 20 deletions cmd/omni/config_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ func configInitCmd() *cobra.Command {
name string
endpoint string
authMethod string
apiKey string
)

cmd := &cobra.Command{
Expand All @@ -57,15 +56,17 @@ func configInitCmd() *cobra.Command {
Long: `Create a new configuration profile.

Prompts interactively for any value not supplied via flags. With --name,
--endpoint, and --auth all set, no prompts are shown.`,
--endpoint, and --auth oauth set, OAuth setup runs with no prompts. For
api-key auth the key is always read from a hidden prompt — it is never
accepted as a flag, so it can't leak into shell history.`,
Example: ` # Interactive setup
omni config init

# Non-interactive OAuth (opens browser for login)
omni config init --name prod --endpoint https://myorg.omniapp.co --auth oauth

# Non-interactive API key
omni config init --name prod --endpoint https://myorg.omniapp.co --api-key "$OMNI_API_TOKEN"`,
# API key (prompts securely for the key)
omni config init --name prod --endpoint https://myorg.omniapp.co --auth api-key`,
RunE: func(cmd *cobra.Command, args []string) error {
reader := bufio.NewReader(os.Stdin)

Expand All @@ -86,8 +87,6 @@ Prompts interactively for any value not supplied via flags. With --name,

choice := strings.TrimSpace(strings.ToLower(authMethod))
switch {
case choice == "" && apiKey != "":
choice = "api-key"
case choice == "":
fmt.Println("Authentication method:")
fmt.Println(" 1) API key")
Expand All @@ -99,10 +98,6 @@ Prompts interactively for any value not supplied via flags. With --name,
return fmt.Errorf("invalid --auth %q — must be %q or %q", authMethod, "api-key", "oauth")
}

if choice == "oauth" && apiKey != "" {
return fmt.Errorf("--api-key cannot be combined with --auth oauth")
}

cfg, err := config.Load()
if err != nil {
cfg = &config.Config{
Expand All @@ -126,15 +121,15 @@ Prompts interactively for any value not supplied via flags. With --name,
cfg.Profiles[name] = p

default: // "1", "a", "api-key", or empty
if apiKey == "" {
fmt.Print("API key: ")
apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return fmt.Errorf("reading API key: %w", err)
}
apiKey = strings.TrimSpace(string(apiKeyBytes))
// The key is always read from a hidden prompt rather than a
// flag, so it never lands in shell history or process listings.
fmt.Print("API key: ")
apiKeyBytes, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
return fmt.Errorf("reading API key: %w", err)
}
apiKey := strings.TrimSpace(string(apiKeyBytes))

cfg.Profiles[name] = config.Profile{
APIEndpoint: endpoint,
Expand All @@ -158,8 +153,7 @@ Prompts interactively for any value not supplied via flags. With --name,

cmd.Flags().StringVar(&name, "name", "", "profile name (skips prompt)")
cmd.Flags().StringVar(&endpoint, "endpoint", "", "API endpoint, e.g. https://myorg.omniapp.co (skips prompt)")
cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt)`)
cmd.Flags().StringVar(&apiKey, "api-key", "", "API key (skips prompt; implies --auth api-key)")
cmd.Flags().StringVar(&authMethod, "auth", "", `authentication method: "api-key" or "oauth" (skips prompt). The API key itself is always read from a hidden prompt, never a flag.`)

return cmd
}
Expand Down
68 changes: 6 additions & 62 deletions cmd/omni/config_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,47 +82,12 @@ func TestApplyOAuthToken_CopiesAllFields(t *testing.T) {
}

// --- config init (non-interactive flags) ---

// With --name, --endpoint, and --api-key all supplied, init must not prompt
// for anything and should write the profile straight to disk.
func TestConfigInit_NonInteractiveAPIKey(t *testing.T) {
withConfig(t, nil)

cmd := configInitCmd()
cmd.SetArgs([]string{
"--name", "prod",
"--endpoint", "https://myorg.omniapp.co",
"--api-key", "sk-test-1234",
})
out := captureStdout(t, func() {
if err := cmd.Execute(); err != nil {
t.Fatalf("Execute: %v", err)
}
})
if strings.Contains(out, "Profile name:") || strings.Contains(out, "Choose [1/2]:") {
t.Errorf("expected no interactive prompts, got:\n%s", out)
}

cfg, err := config.Load()
if err != nil {
t.Fatalf("Load: %v", err)
}
p := cfg.Profiles["prod"]
if p.APIEndpoint != "https://myorg.omniapp.co" {
t.Errorf("APIEndpoint = %q", p.APIEndpoint)
}
// --api-key alone implies --auth api-key.
if p.AuthMethod != "api-key" {
t.Errorf("AuthMethod = %q, want %q", p.AuthMethod, "api-key")
}
if p.APIKey != "sk-test-1234" {
t.Errorf("APIKey = %q", p.APIKey)
}
// First profile becomes the default.
if cfg.DefaultProfile != "prod" {
t.Errorf("DefaultProfile = %q, want %q", cfg.DefaultProfile, "prod")
}
}
//
// Note: there is deliberately no --api-key flag. Accepting a secret on the
// command line leaks it into shell history and process listings (and from
// there into anything that scrapes those, including agentic tooling). The key
// is always read from a hidden prompt instead; only the non-secret --name,
// --endpoint, and --auth values can be supplied non-interactively.

// --auth oauth must validate the endpoint BEFORE launching the browser flow,
// so a bad endpoint fails fast instead of opening a browser against it.
Expand Down Expand Up @@ -167,27 +132,6 @@ func TestConfigInit_InvalidAuthFlag(t *testing.T) {
}
}

func TestConfigInit_APIKeyConflictsWithOAuth(t *testing.T) {
withConfig(t, nil)

cmd := configInitCmd()
cmd.SilenceUsage = true
cmd.SilenceErrors = true
cmd.SetArgs([]string{
"--name", "prod",
"--endpoint", "https://myorg.omniapp.co",
"--auth", "oauth",
"--api-key", "sk-test-1234",
})
err := cmd.Execute()
if err == nil {
t.Fatal("expected conflict error, got nil")
}
if !strings.Contains(err.Error(), "--api-key cannot be combined") {
t.Errorf("error = %q, want api-key/oauth conflict message", err.Error())
}
}

// --- config logout ---

func TestConfigLogout_ClearsTokensForNamedProfile(t *testing.T) {
Expand Down
Loading