vaultez-cli 0.3.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f51a620534fdfedc6d42e56c4bfadf38f0705298f984a96326de81967d6b893
4
- data.tar.gz: 5cd7deade6d544287ebe947bc9ad18031ab014dcdd3bd7b74b0de7b489f9f183
3
+ metadata.gz: 7ec4178a4e79116eef6ff1b13b8db47f134cb9c57ed0fa85e727871886acb2fa
4
+ data.tar.gz: 23ee0ad5bf641a03a7a8cc3763ab1b2c140c50a07927dac808eded42d2e7a421
5
5
  SHA512:
6
- metadata.gz: afe76438bda1a5bbffe795105685b7ef952e96012c9bb8841827d6d25fb75b00aeb83d7c3e88aaa16a5617d1ec73bf868eb43de2be0e1441f28cf522938df5c2
7
- data.tar.gz: 8f5617fbe1654c3ffb4b38e40cb623d656c610c36a0f0cb0019bf90b3c12e121bb4fc733f3524af5bc89dbe2a1242d437f2cd06d79fd9969210db27e88b86fb2
6
+ metadata.gz: b4e00d6f19821fffe29bfac9430b125a9dbab05f8e3a2d53f73bdae8e4c6adb820999ef32d563d3825601978df128d31ef827b33a07aa267d21f1e4c182f7e00
7
+ data.tar.gz: e53532d610033a006a3f04e8153d35e081b1605fd55ed7f5de4b813a0cf67f4f3191b0206844cd068a0e14c0912eed9d4e241aa1e942e21149d77342767a4873
data/lib/vaultez/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require "thor"
2
2
  require_relative "version"
3
+ require_relative "output"
3
4
  require_relative "commands/auth"
4
5
  require_relative "commands/fetch"
5
6
  require_relative "commands/config_command"
@@ -12,6 +13,12 @@ module Vaultez
12
13
 
13
14
  map %w[--version -v] => :__version
14
15
 
16
+ # Usage errors (unknown flag, bad --format) must exit non-zero, or
17
+ # `vaultez fetch ... > .env && ./start.sh` carries on as if it worked.
18
+ def self.exit_on_failure?
19
+ true
20
+ end
21
+
15
22
  desc "login", "Authenticate with email, password, and 2FA code"
16
23
  long_desc <<~DESC, wrap: false
17
24
  Authenticate with your Vaultez account. You will be prompted for your
@@ -54,6 +61,26 @@ module Vaultez
54
61
  VAULTEZ_TOKEN=vz_... vaultez fetch
55
62
  VAULTEZ_TOKEN=vz_... vaultez fetch --secret="DATABASE_URL"
56
63
 
64
+ OUTPUT FORMATS (--format, for a project's secrets):
65
+ env KEY='value' lines, quoted so they are safe to eval or source (default)
66
+ eval "$(vaultez fetch --project="Backend")"
67
+ set -a; source <(vaultez fetch --project="Backend"); set +a
68
+ shell export KEY='value' lines
69
+ dotenv a .env file for dotenv, Next.js, Vite or Docker Compose
70
+ vaultez fetch --project="Backend" --format=dotenv > .env.local
71
+ github entries for $GITHUB_ENV, multi-line values included
72
+ vaultez fetch --format=github >> "$GITHUB_ENV"
73
+ json a JSON array of {id, name, value} (same as --json)
74
+
75
+ Secrets whose names aren't valid variable names (letters, numbers and _,
76
+ not starting with a number) are skipped, with a warning on stderr. They
77
+ are still included in json output.
78
+
79
+ --secret prints just the value, unquoted, or a JSON object with --format=json.
80
+
81
+ Stdout only ever carries data. Errors and notices such as "No secrets
82
+ found" go to stderr, and errors exit with status 1.
83
+
57
84
  Project tokens can be created in the Tokens tab of your project settings.
58
85
  Always pass the token via the VAULTEZ_TOKEN environment variable, never
59
86
  as a command-line flag — CLI arguments are visible to other local users
@@ -64,7 +91,8 @@ module Vaultez
64
91
  option :projects, type: :boolean, desc: "List projects in a company"
65
92
  option :project, type: :string, desc: "Project name"
66
93
  option :secret, type: :string, desc: "Secret name (returns value only)"
67
- option :json, type: :boolean, desc: "Output as JSON (errors go to stderr)"
94
+ option :format, type: :string, enum: Vaultez::Output::FORMATS, desc: "Output format (default: env)"
95
+ option :json, type: :boolean, desc: "Output as JSON (same as --format=json)"
68
96
  def fetch; super; end
69
97
 
70
98
  desc "config", "Set default company or token"
@@ -20,11 +20,11 @@ module Vaultez
20
20
  Vaultez::Config.set("token", response["token"])
21
21
  puts "Logged in successfully."
22
22
  rescue Vaultez::TwoFactorRequiredError => error
23
- puts "Error: #{error.message}"
24
- puts "Set up two-factor authentication at https://vaultez.app/two_factor/new"
23
+ warn "Error: #{error.message}"
24
+ warn "Set up two-factor authentication at https://vaultez.app/two_factor/new"
25
25
  exit 1
26
26
  rescue Vaultez::AuthenticationError => error
27
- puts "Error: #{error.message}"
27
+ warn "Error: #{error.message}"
28
28
  exit 1
29
29
  end
30
30
 
@@ -34,7 +34,7 @@ module Vaultez
34
34
  Vaultez::Config.clear
35
35
  puts "Logged out successfully."
36
36
  rescue Vaultez::NotAuthenticatedError => error
37
- puts "Error: #{error.message}"
37
+ warn "Error: #{error.message}"
38
38
  exit 1
39
39
  end
40
40
  end
@@ -8,21 +8,21 @@ module Vaultez
8
8
  company = companies.find { |company| company["name"] == options[:"default-company"] }
9
9
 
10
10
  unless company
11
- puts "Error: company \"#{options[:"default-company"]}\" not found."
11
+ warn "Error: company \"#{options[:"default-company"]}\" not found."
12
12
  exit 1
13
13
  end
14
14
 
15
15
  Vaultez::Config.set("default_company", company["name"])
16
16
  puts "Default company set to \"#{company["name"]}\"."
17
17
  else
18
- puts "No config option provided. See `vaultez help config`."
18
+ warn "No config option provided. See `vaultez help config`."
19
19
  exit 1
20
20
  end
21
21
  rescue Vaultez::NotAuthenticatedError => error
22
- puts "Error: #{error.message}"
22
+ warn "Error: #{error.message}"
23
23
  exit 1
24
24
  rescue Vaultez::ApiError => error
25
- puts "Error: #{error.message}"
25
+ warn "Error: #{error.message}"
26
26
  exit 1
27
27
  end
28
28
  end
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require_relative "../output"
2
3
 
3
4
  module Vaultez
4
5
  module Commands
@@ -29,13 +30,40 @@ module Vaultez
29
30
 
30
31
  private
31
32
 
33
+ # Stdout carries only data, so `vaultez fetch > .env` never writes an
34
+ # error or a notice into the file. Everything else goes to stderr.
32
35
  def fail!(message)
33
- if options[:json]
34
- warn "Error: #{message}"
36
+ warn "Error: #{message}"
37
+ exit 1
38
+ end
39
+
40
+ def output_format
41
+ @output_format ||= begin
42
+ format = options[:format]
43
+ if options[:json] && format && format != "json"
44
+ fail!("--json can't be combined with --format=#{format}.")
45
+ end
46
+ format || (options[:json] ? "json" : Vaultez::Output::DEFAULT_FORMAT)
47
+ end
48
+ end
49
+
50
+ def json?
51
+ output_format == "json"
52
+ end
53
+
54
+ def print_secret_value(secret)
55
+ if json?
56
+ puts secret.to_json
35
57
  else
36
- puts "Error: #{message}"
58
+ print secret["value"]
37
59
  end
38
- exit 1
60
+ end
61
+
62
+ def print_secrets(secrets, empty_message)
63
+ warn empty_message if secrets.empty?
64
+ text, warnings = Vaultez::Output.render(secrets, output_format)
65
+ print text
66
+ warnings.each { |message| warn "Warning: #{message}" }
39
67
  end
40
68
 
41
69
  def fetch_with_project_token(client)
@@ -45,22 +73,9 @@ module Vaultez
45
73
  unless secret
46
74
  fail!("secret \"#{options[:secret]}\" not found.")
47
75
  end
48
- if options[:json]
49
- puts secret.to_json
50
- else
51
- print secret["value"]
52
- end
76
+ print_secret_value(secret)
53
77
  else
54
- secrets = fetch_all_secrets_for_token(client)
55
- if options[:json]
56
- puts secrets.to_json
57
- return
58
- end
59
- if secrets.empty?
60
- puts "No secrets found."
61
- return
62
- end
63
- secrets.each { |s| puts "#{s["name"]}=#{s["value"]}" }
78
+ print_secrets(fetch_all_secrets_for_token(client), "No secrets found.")
64
79
  end
65
80
  end
66
81
 
@@ -80,12 +95,12 @@ module Vaultez
80
95
 
81
96
  def fetch_companies(client)
82
97
  companies = client.companies
83
- if options[:json]
98
+ if json?
84
99
  puts companies.to_json
85
100
  return
86
101
  end
87
102
  if companies.empty?
88
- puts "No companies found."
103
+ warn "No companies found."
89
104
  return
90
105
  end
91
106
  puts "Companies:"
@@ -97,12 +112,12 @@ module Vaultez
97
112
  def fetch_projects(client)
98
113
  company = resolve_company(client)
99
114
  projects = client.projects(company["id"])
100
- if options[:json]
115
+ if json?
101
116
  puts projects.to_json
102
117
  return
103
118
  end
104
119
  if projects.empty?
105
- puts "No projects found in #{company["name"]}."
120
+ warn "No projects found in #{company["name"]}."
106
121
  return
107
122
  end
108
123
  puts "Projects in #{company["name"]}:"
@@ -115,17 +130,7 @@ module Vaultez
115
130
  company = resolve_company(client)
116
131
  project = resolve_project(client, company)
117
132
  secrets = client.secrets(project["id"])
118
- if options[:json]
119
- puts secrets.to_json
120
- return
121
- end
122
- if secrets.empty?
123
- puts "No secrets found in #{project["name"]}."
124
- return
125
- end
126
- secrets.each do |secret|
127
- puts "#{secret["name"]}=#{secret["value"]}"
128
- end
133
+ print_secrets(secrets, "No secrets found in #{project["name"]}.")
129
134
  end
130
135
 
131
136
  def fetch_secret(client)
@@ -138,11 +143,7 @@ module Vaultez
138
143
  fail!("secret \"#{options[:secret]}\" not found in #{project["name"]}.")
139
144
  end
140
145
 
141
- if options[:json]
142
- puts secret.to_json
143
- else
144
- print secret["value"]
145
- end
146
+ print_secret_value(secret)
146
147
  end
147
148
 
148
149
  def resolve_company(client)
@@ -0,0 +1,88 @@
1
+ require "json"
2
+ require "securerandom"
3
+
4
+ module Vaultez
5
+ # Renders a project's secrets for the formats `vaultez fetch --format=...`
6
+ # accepts. Every format except json must be safe to hand to its consumer
7
+ # as-is: a secret value is data written by any project editor, so it must
8
+ # never be able to run a command, end its own line early, or set a second
9
+ # variable on the machine of whoever sources the output.
10
+ module Output
11
+ FORMATS = %w[env shell dotenv github json].freeze
12
+ DEFAULT_FORMAT = "env".freeze
13
+
14
+ VARIABLE_NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/
15
+
16
+ module_function
17
+
18
+ def valid_name?(name)
19
+ name.is_a?(String) && VARIABLE_NAME.match?(name)
20
+ end
21
+
22
+ # Returns [text, warnings]; the caller prints the warnings on stderr.
23
+ # Secrets whose names can't be variable names are left out of the
24
+ # non-json formats, since no shell or dotenv parser would accept the line.
25
+ def render(secrets, format)
26
+ return [JSON.generate(secrets) + "\n", []] if format == "json"
27
+
28
+ warnings = []
29
+ lines = secrets.filter_map do |secret|
30
+ name, value = secret["name"], secret["value"].to_s
31
+ unless valid_name?(name)
32
+ warnings << "skipped secret #{name.inspect}: not a valid variable name. " \
33
+ "Rename it to use only letters, numbers and _ (not starting with a number), or use --format=json."
34
+ next
35
+ end
36
+ if format == "dotenv" && !portable_in_dotenv?(value)
37
+ warnings << "secret #{name.inspect} contains ' together with \", \\ or $. Ruby dotenv and " \
38
+ "Docker Compose read it correctly; Node's dotenv keeps the backslash escapes."
39
+ end
40
+ line(name, value, format)
41
+ end
42
+ [lines.join, warnings]
43
+ end
44
+
45
+ def line(name, value, format)
46
+ case format
47
+ when "env" then "#{name}=#{shell_quote(value)}\n"
48
+ when "shell" then "export #{name}=#{shell_quote(value)}\n"
49
+ when "dotenv" then "#{name}=#{dotenv_quote(value)}\n"
50
+ when "github" then github_entry(name, value)
51
+ else raise ArgumentError, "unknown format: #{format}"
52
+ end
53
+ end
54
+
55
+ # POSIX single quotes: nothing inside is special except the closing
56
+ # quote itself, which is written as '\'' (close, escaped quote, reopen).
57
+ def shell_quote(value)
58
+ "'#{value.gsub("'", %q('\\\\''))}'"
59
+ end
60
+
61
+ # Single-quoted values, line breaks included, are literal in every
62
+ # common dotenv parser (Ruby dotenv, Node dotenv, Docker Compose), so use
63
+ # them whenever the value has no ' of its own. Double quotes are next:
64
+ # also literal as long as there's no ", \ or $ to interpret. Only a value
65
+ # with both needs escapes, and parsers disagree on those: Ruby dotenv and
66
+ # Compose unescape \" \\ \$ (and Ruby runs $(...) if $ isn't escaped),
67
+ # while Node's dotenv leaves the backslashes in.
68
+ def dotenv_quote(value)
69
+ return "'#{value}'" unless value.include?("'")
70
+ return "\"#{value}\"" unless value.match?(/["\\$]/)
71
+
72
+ "\"#{value.gsub(/["\\$]/) { |char| "\\#{char}" }}\""
73
+ end
74
+
75
+ def portable_in_dotenv?(value)
76
+ !(value.include?("'") && value.match?(/["\\$]/))
77
+ end
78
+
79
+ # GitHub's multiline syntax for $GITHUB_ENV. A random delimiter means a
80
+ # value can't close the block early and smuggle in another variable
81
+ # (e.g. NODE_OPTIONS) for later workflow steps.
82
+ def github_entry(name, value)
83
+ delimiter = "VAULTEZ_EOF_#{SecureRandom.hex(16)}"
84
+ delimiter = "VAULTEZ_EOF_#{SecureRandom.hex(16)}" while value.include?(delimiter)
85
+ "#{name}<<#{delimiter}\n#{value}\n#{delimiter}\n"
86
+ end
87
+ end
88
+ end
@@ -1,3 +1,3 @@
1
1
  module Vaultez
2
- VERSION = "0.3.2"
2
+ VERSION = "0.4.0"
3
3
  end
data/lib/vaultez.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require_relative "vaultez/version"
2
2
  require_relative "vaultez/errors"
3
+ require_relative "vaultez/output"
3
4
  require_relative "vaultez/config"
4
5
  require_relative "vaultez/client"
5
6
  require_relative "vaultez/cli"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vaultez-cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.2
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nur Ketene
@@ -39,6 +39,7 @@ files:
39
39
  - lib/vaultez/commands/fetch.rb
40
40
  - lib/vaultez/config.rb
41
41
  - lib/vaultez/errors.rb
42
+ - lib/vaultez/output.rb
42
43
  - lib/vaultez/version.rb
43
44
  homepage: https://vaultez.app
44
45
  licenses:
@@ -58,7 +59,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
58
59
  - !ruby/object:Gem::Version
59
60
  version: '0'
60
61
  requirements: []
61
- rubygems_version: 4.0.16
62
+ rubygems_version: 4.0.20
62
63
  specification_version: 4
63
64
  summary: CLI tool for Vaultez — manage your secrets from the terminal
64
65
  test_files: []