diff --git a/packages/cmd/bootstrap.go b/packages/cmd/bootstrap.go index 40b5b6e0..48af8239 100644 --- a/packages/cmd/bootstrap.go +++ b/packages/cmd/bootstrap.go @@ -171,11 +171,9 @@ var bootstrapCmd = &cobra.Command{ return } - domain, _ := cmd.Flags().GetString("domain") - if domain == "" { - if envDomain, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - domain = envDomain - } + domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", util.DomainEnvNames, "") + if err != nil { + util.HandleError(err, "Unable to parse domain") } if domain == "" { diff --git a/packages/cmd/kmip.go b/packages/cmd/kmip.go index cc1851f7..019370c3 100644 --- a/packages/cmd/kmip.go +++ b/packages/cmd/kmip.go @@ -138,7 +138,7 @@ var kmipSystemdInstallCmd = &cobra.Command{ util.HandleError(err, "Unable to parse identity client secret") } - domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", []string{util.INFISICAL_API_URL_ENV_NAME}, "") + domain, err := util.GetCmdFlagOrEnvWithDefaultValue(cmd, "domain", util.DomainEnvNames, "") if err != nil { util.HandleError(err, "Unable to parse domain") } diff --git a/packages/cmd/login.go b/packages/cmd/login.go index 979f50d3..873efce5 100644 --- a/packages/cmd/login.go +++ b/packages/cmd/login.go @@ -439,7 +439,7 @@ func DomainOverridePrompt() (bool, error) { //trim the '/' from the end of the domain url config.INFISICAL_URL_MANUAL_OVERRIDE = strings.TrimRight(config.INFISICAL_URL_MANUAL_OVERRIDE, "/") optionsPrompt := promptui.Select{ - Label: fmt.Sprintf("Current INFISICAL_API_URL Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), + Label: fmt.Sprintf("Current Domain Override: %s", config.INFISICAL_URL_MANUAL_OVERRIDE), Items: options, Size: 2, } @@ -491,7 +491,7 @@ func usePresetDomain(presetDomain string, domainFlagExplicitlySet bool, shouldPr boldWhite := whilte.Add(color.Bold) time.Sleep(time.Second * 1) if shouldPrintInfo { - boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_API_URL environment variable\n", parsedDomain) + boldWhite.Printf("[INFO] Using domain '%s' from domain flag or INFISICAL_DOMAIN environment variable\n", parsedDomain) } return true, nil diff --git a/packages/cmd/login_status.go b/packages/cmd/login_status.go index 62bda7ac..1c269f6a 100644 --- a/packages/cmd/login_status.go +++ b/packages/cmd/login_status.go @@ -69,8 +69,10 @@ func runLoginStatus(cmd *cobra.Command, args []string) { flagToken = strings.TrimSpace(flagToken) if flagToken != "" { if !cmd.Flags().Changed("domain") { - if _, envSet := os.LookupEnv("INFISICAL_API_URL"); !envSet { - util.PrintErrorMessageAndExit("--token requires --domain (or INFISICAL_API_URL) to be set so the status reflects the correct Infisical instance") + _, envSet := util.GetEnvDomain() + workspaceConfig, _ := util.GetWorkSpaceFromFile() + if !envSet && workspaceConfig.Domain == "" { + util.PrintErrorMessageAndExit("--token requires the Infisical instance to be set via --domain, the INFISICAL_DOMAIN env var, or the 'domain' field in .infisical.json so the status reflects the correct instance") } } ctx, err := buildContextFromToken(flagToken, "--token flag", envDomain) diff --git a/packages/cmd/root.go b/packages/cmd/root.go index 1a8bfa62..de1b0b82 100644 --- a/packages/cmd/root.go +++ b/packages/cmd/root.go @@ -88,13 +88,41 @@ func Execute() { } } +// resolveDomain picks the domain by precedence: --domain flag > env > .infisical.json > default (flagValue). +// Must run after flag parsing (PersistentPreRun, not init) so cmd.Flags().Changed is reliable. +func resolveDomain(cmd *cobra.Command, flagValue string) string { + if cmd.Flags().Changed("domain") { + return flagValue + } + + if envDomain, ok := util.GetEnvDomain(); ok { + return envDomain + } + + workspaceConfig, err := util.GetWorkSpaceFromFile() + if err != nil || workspaceConfig.Domain == "" { + return flagValue + } + + domain := workspaceConfig.Domain + if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") { + util.PrintWarningWithWriter("The 'domain' field in .infisical.json is not a valid URL (must start with http:// or https://). It will be ignored.", cmd.ErrOrStderr()) + return flagValue + } + + // A .infisical.json is usually committed to the repo, so a malicious one could redirect requests + // and credentials. Always surface where traffic is going (even under --silent); it goes to stderr. + util.PrintWarningWithWriter(fmt.Sprintf("Using domain '%s' from .infisical.json; all requests and credentials will be sent there.", domain), cmd.ErrOrStderr()) + return domain +} + func init() { util.GetStderrWriter = RootCmdStderrWriter util.GetStdoutWriter = RootCmdStdoutWriter cobra.OnInitialize(initLog) RootCmd.PersistentFlags().StringP("log-level", "l", "", "log level (trace, debug, info, warn, error, fatal)") RootCmd.PersistentFlags().Bool("telemetry", true, "Infisical collects non-sensitive telemetry data to enhance features and improve user experience. Participation is voluntary") - RootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your Infisical instance (e.g., https://eu.infisical.com for EU Cloud, or https://your-instance.com for self-hosted). Can also set via INFISICAL_API_URL environment variable. Required for non-US Cloud users.") + RootCmd.PersistentFlags().StringVar(&config.INFISICAL_URL, "domain", fmt.Sprintf("%s/api", util.INFISICAL_DEFAULT_US_URL), "Point the CLI to your Infisical instance (e.g., https://eu.infisical.com for EU Cloud, or https://your-instance.com for self-hosted). Can also set via INFISICAL_DOMAIN environment variable or the 'domain' field in .infisical.json. Required for non-US Cloud users.") RootCmd.PersistentFlags().Bool("silent", false, "Disable output of tip/info messages. Useful when running in scripts or CI/CD pipelines.") RootCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { silent, err := cmd.Flags().GetBool("silent") @@ -102,7 +130,7 @@ func init() { util.HandleError(err) } - config.INFISICAL_URL = util.AppendAPIEndpoint(config.INFISICAL_URL) + config.INFISICAL_URL = util.AppendAPIEndpoint(resolveDomain(cmd, config.INFISICAL_URL)) // util.DisplayAptInstallationChangeBannerWithWriter(silent, cmd.ErrOrStderr()) if !util.IsRunningInDocker() && !silent && !isStructuredOutputRequested(cmd) { @@ -121,14 +149,6 @@ func init() { } - // if config.INFISICAL_URL is set to the default value, check if INFISICAL_URL is set in the environment - // this is used to allow overrides of the default value - if !RootCmd.Flag("domain").Changed { - if envInfisicalBackendUrl, ok := os.LookupEnv("INFISICAL_API_URL"); ok { - config.INFISICAL_URL = util.AppendAPIEndpoint(envInfisicalBackendUrl) - } - } - isTelemetryOn, _ := RootCmd.PersistentFlags().GetBool("telemetry") Telemetry = telemetry.NewTelemetry(isTelemetryOn) } diff --git a/packages/models/cli.go b/packages/models/cli.go index ea874088..16aed48a 100644 --- a/packages/models/cli.go +++ b/packages/models/cli.go @@ -112,6 +112,7 @@ type WorkspaceConfigFile struct { WorkspaceId string `json:"workspaceId"` DefaultEnvironment string `json:"defaultEnvironment"` GitBranchToEnvironmentMapping map[string]string `json:"gitBranchToEnvironmentMapping"` + Domain string `json:"domain,omitempty"` } type SymmetricEncryptionResult struct { diff --git a/packages/util/config_test.go b/packages/util/config_test.go new file mode 100644 index 00000000..cc7e6af8 --- /dev/null +++ b/packages/util/config_test.go @@ -0,0 +1,72 @@ +package util + +import ( + "os" + "testing" +) + +func TestWorkspaceConfigDomain(t *testing.T) { + cases := []struct { + name string + path string + wantDomain string + }{ + {"domain field is parsed", "testdata/infisical-with-domain.json", "https://custom.infisical.com"}, + {"existing config without a domain field parses to empty", "testdata/infisical-default-env.json", ""}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg, err := GetWorkspaceConfigByPath(tc.path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if cfg.Domain != tc.wantDomain { + t.Errorf("Domain = %q, want %q", cfg.Domain, tc.wantDomain) + } + }) + } +} + +func TestGetEnvDomain(t *testing.T) { + const unset = "\x00" // sentinel: leave the env var unset for this case + + cases := []struct { + name string + domain string // INFISICAL_DOMAIN + apiURL string // INFISICAL_API_URL (legacy) + wantVal string + wantOk bool + }{ + {"prefers INFISICAL_DOMAIN over legacy", "https://domain.infisical.com", "https://apiurl.infisical.com", "https://domain.infisical.com", true}, + {"falls back to legacy INFISICAL_API_URL", unset, "https://apiurl.infisical.com", "https://apiurl.infisical.com", true}, + {"blank INFISICAL_DOMAIN falls through to legacy", " ", "https://apiurl.infisical.com", "https://apiurl.infisical.com", true}, + {"neither set", unset, unset, "", false}, + {"both blank are treated as unset", " ", " ", "", false}, + } + + setOrUnset := func(t *testing.T, key, val string) { + t.Helper() + t.Setenv(key, "") // register restore-on-cleanup, then mutate freely below + if val == unset { + os.Unsetenv(key) + return + } + os.Setenv(key, val) + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + setOrUnset(t, INFISICAL_DOMAIN_ENV_NAME, tc.domain) + setOrUnset(t, LEGACY_INFISICAL_API_URL_ENV_NAME, tc.apiURL) + + got, ok := GetEnvDomain() + if ok != tc.wantOk { + t.Fatalf("ok = %v, want %v", ok, tc.wantOk) + } + if got != tc.wantVal { + t.Errorf("value = %q, want %q", got, tc.wantVal) + } + }) + } +} diff --git a/packages/util/constants.go b/packages/util/constants.go index 4bf0c9e7..22bdf606 100644 --- a/packages/util/constants.go +++ b/packages/util/constants.go @@ -49,7 +49,8 @@ const ( // Generic env variable used for auth methods that require a machine identity ID INFISICAL_MACHINE_IDENTITY_ID_NAME = "INFISICAL_MACHINE_IDENTITY_ID" - INFISICAL_API_URL_ENV_NAME = "INFISICAL_API_URL" + INFISICAL_DOMAIN_ENV_NAME = "INFISICAL_DOMAIN" + LEGACY_INFISICAL_API_URL_ENV_NAME = "INFISICAL_API_URL" // superseded by INFISICAL_DOMAIN; kept for backwards compatibility SECRET_TYPE_PERSONAL = "personal" SECRET_TYPE_SHARED = "shared" diff --git a/packages/util/helper.go b/packages/util/helper.go index ced0dbcc..1babadef 100644 --- a/packages/util/helper.go +++ b/packages/util/helper.go @@ -456,6 +456,21 @@ func getCurrentBranch() (string, error) { return path.Base(strings.TrimSpace(out.String())), nil } +// DomainEnvNames lists the env vars that configure the Infisical domain, in +// precedence order: INFISICAL_DOMAIN first, then the legacy INFISICAL_API_URL. +var DomainEnvNames = []string{INFISICAL_DOMAIN_ENV_NAME, LEGACY_INFISICAL_API_URL_ENV_NAME} + +// GetEnvDomain returns the Infisical domain configured via environment +// variables, preferring INFISICAL_DOMAIN over the legacy INFISICAL_API_URL. +func GetEnvDomain() (string, bool) { + for _, env := range DomainEnvNames { + if domain := strings.TrimSpace(os.Getenv(env)); domain != "" { + return domain, true + } + } + return "", false +} + func AppendAPIEndpoint(address string) string { // if it's empty return as it is // Ensure the address does not already end with "/api" diff --git a/packages/util/testdata/infisical-with-domain.json b/packages/util/testdata/infisical-with-domain.json new file mode 100644 index 00000000..679747bb --- /dev/null +++ b/packages/util/testdata/infisical-with-domain.json @@ -0,0 +1,6 @@ +{ + "workspaceId": "test-workspace-id", + "defaultEnvironment": "dev", + "gitBranchToEnvironmentMapping": null, + "domain": "https://custom.infisical.com" +}