localvault 1.0.3 → 1.0.4

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: d023e44aaf39e8d923fc05d81dab1e3abd2565ef9ad88f27bd81dc585fae9e89
4
- data.tar.gz: a2bfb0fd94b29c4046c66c969ac044b81084cd2ed841900f185ca7c4b997fcb5
3
+ metadata.gz: 20ee40aedd506f5c39aa06297c8bbcd53ea816d81e6843d96b5749e9014e5ef3
4
+ data.tar.gz: 6b8a4cbcc8178b9f173e1152355084886a9b66d086c981699cfafaf2a37e2d7d
5
5
  SHA512:
6
- metadata.gz: 593592ada6728cc3f28d5f57c95bd4ce0c964b272dee714e9c7432810143567fcdc62c19c5566fa5061080d5f0f7c1bd4337be42c882ed94589e571a7ce506b8
7
- data.tar.gz: 3db6aba11ba118de7603236a3a5446b471a5b28c487ba938f4940953c3e457dcc725b14f229a4e83806f1e81c5126d68816b1e214ffed1f674f372e52bc2fa2b
6
+ metadata.gz: 845e91a27ad02a21a88b68ad41c4691ba8cc9661aadd9ef81d85a383acf02e186d14c69f2fa7bb5ad3371b68f1de786ff4bcf0dad7d4e3b21133e9effb965a7e
7
+ data.tar.gz: 7cedfdab731d01dfcc1f4eaf9357beb17fb5e749dc06aec7091d6399ca7336717e19f681f68827bd437d2cd0c5f07a8f87a69feb5c4118acc1babaf78e527a21
data/README.md CHANGED
@@ -176,7 +176,7 @@ If you've already run `eval $(localvault unlock)` in your terminal, the agent in
176
176
 
177
177
  **Available tools:** `get_secret`, `list_secrets`, `set_secret`, `delete_secret`
178
178
 
179
- See [MCP Setup Guide](docs/site-docs/mcp-setup.md) for Claude Code and Cursor configuration details.
179
+ See [MCP for AI Agents](https://inventlist.com/sites/localvault/series/localvault/mcp-for-ai-agents) for Claude Code and Cursor configuration details.
180
180
 
181
181
  ## Security
182
182
 
@@ -122,8 +122,9 @@ module LocalVault
122
122
  uri = URI("#{@base_url}#{BASE_PATH}#{path}")
123
123
  http = Net::HTTP.new(uri.host, uri.port)
124
124
  http.use_ssl = uri.scheme == "https"
125
- http.open_timeout = 10
126
- http.read_timeout = 30
125
+ http.open_timeout = 10
126
+ http.read_timeout = 30
127
+ http.write_timeout = 30
127
128
 
128
129
  req_class = {
129
130
  get: Net::HTTP::Get,
@@ -154,8 +155,9 @@ module LocalVault
154
155
  uri = URI("#{@base_url}#{BASE_PATH}#{path}")
155
156
  http = Net::HTTP.new(uri.host, uri.port)
156
157
  http.use_ssl = uri.scheme == "https"
157
- http.open_timeout = 10
158
- http.read_timeout = 30
158
+ http.open_timeout = 10
159
+ http.read_timeout = 30
160
+ http.write_timeout = 30
159
161
 
160
162
  req_class = { put: Net::HTTP::Put }.fetch(method)
161
163
  req = req_class.new(uri.request_uri)
@@ -179,8 +181,9 @@ module LocalVault
179
181
  uri = URI("#{@base_url}#{BASE_PATH}#{path}")
180
182
  http = Net::HTTP.new(uri.host, uri.port)
181
183
  http.use_ssl = uri.scheme == "https"
182
- http.open_timeout = 10
183
- http.read_timeout = 30
184
+ http.open_timeout = 10
185
+ http.read_timeout = 30
186
+ http.write_timeout = 30
184
187
 
185
188
  req_class = { get: Net::HTTP::Get }.fetch(method)
186
189
  req = req_class.new(uri.request_uri)
@@ -40,7 +40,7 @@ module LocalVault
40
40
 
41
41
  client = ApiClient.new(token: Config.token)
42
42
  blob = client.pull_vault(vault_name)
43
- data = SyncBundle.unpack(blob)
43
+ data = SyncBundle.unpack(blob, expected_name: vault_name)
44
44
 
45
45
  FileUtils.mkdir_p(store.vault_path, mode: 0o700)
46
46
  File.write(store.meta_path, data[:meta])
@@ -53,6 +53,8 @@ module LocalVault
53
53
 
54
54
  $stdout.puts "Pulled vault '#{vault_name}'."
55
55
  $stdout.puts "Unlock it with: localvault unlock -v #{vault_name}"
56
+ rescue SyncBundle::UnpackError => e
57
+ $stderr.puts "Error: #{e.message}"
56
58
  rescue ApiClient::ApiError => e
57
59
  if e.status == 404
58
60
  $stderr.puts "Error: Vault '#{vault_name}' not found in cloud."
@@ -161,7 +161,8 @@ module LocalVault
161
161
  method_option :project, aliases: "-p", type: :string, desc: "Export only this project group (no prefix)"
162
162
  def env
163
163
  vault = open_vault!
164
- $stdout.puts vault.export_env(project: options[:project])
164
+ skip_warn = ->(k) { $stderr.puts "Warning: skipping unsafe key '#{k}'" }
165
+ $stdout.puts vault.export_env(project: options[:project], on_skip: skip_warn)
165
166
  end
166
167
 
167
168
  desc "exec -- CMD", "Run a command with secrets injected as environment variables"
@@ -184,7 +185,8 @@ module LocalVault
184
185
  method_option :project, aliases: "-p", type: :string, desc: "Inject only this project group (no prefix)"
185
186
  def exec(*cmd)
186
187
  vault = open_vault!
187
- env_vars = vault.env_hash(project: options[:project])
188
+ skip_warn = ->(k) { $stderr.puts "Warning: skipping unsafe key '#{k}'" }
189
+ env_vars = vault.env_hash(project: options[:project], on_skip: skip_warn)
188
190
  Kernel.exec(env_vars, *cmd)
189
191
  end
190
192
 
@@ -27,7 +27,7 @@ module LocalVault
27
27
  end
28
28
 
29
29
  def self.save(data)
30
- FileUtils.mkdir_p(root_path)
30
+ FileUtils.mkdir_p(root_path, mode: 0o700)
31
31
  File.write(config_path, YAML.dump(data))
32
32
  File.chmod(0o600, config_path)
33
33
  end
@@ -21,7 +21,10 @@ module LocalVault
21
21
  return nil unless key_b64 && expiry_str
22
22
 
23
23
  expiry = expiry_str.to_i
24
- return nil if Time.now.to_i >= expiry
24
+ if Time.now.to_i >= expiry
25
+ clear(vault_name) # clean up expired entry
26
+ return nil
27
+ end
25
28
 
26
29
  Base64.strict_decode64(key_b64)
27
30
  rescue ArgumentError
@@ -1,5 +1,6 @@
1
1
  require "json"
2
2
  require "base64"
3
+ require "yaml"
3
4
 
4
5
  module LocalVault
5
6
  module SyncBundle
@@ -20,14 +21,24 @@ module LocalVault
20
21
  end
21
22
 
22
23
  # Unpack a blob back into {meta:, secrets:} strings.
23
- def self.unpack(blob)
24
+ # Pass expected_name: to validate the meta.yml name matches the vault being pulled.
25
+ def self.unpack(blob, expected_name: nil)
24
26
  data = JSON.parse(blob)
25
27
  version = data["version"]
26
28
  raise UnpackError, "Unsupported bundle version: #{version}" if version && version != VERSION
27
- {
28
- meta: Base64.strict_decode64(data.fetch("meta")),
29
- secrets: Base64.strict_decode64(data.fetch("secrets"))
30
- }
29
+
30
+ meta_raw = Base64.strict_decode64(data.fetch("meta"))
31
+ secrets_raw = Base64.strict_decode64(data.fetch("secrets"))
32
+
33
+ if expected_name
34
+ meta_parsed = YAML.safe_load(meta_raw)
35
+ actual_name = meta_parsed&.dig("name")
36
+ if actual_name && actual_name != expected_name
37
+ raise UnpackError, "Bundle meta name '#{actual_name}' does not match expected vault '#{expected_name}'"
38
+ end
39
+ end
40
+
41
+ { meta: meta_raw, secrets: secrets_raw }
31
42
  rescue JSON::ParserError => e
32
43
  raise UnpackError, "Invalid sync bundle format: #{e.message}"
33
44
  rescue KeyError => e
@@ -76,20 +76,43 @@ module LocalVault
76
76
  # Export as shell variable assignments.
77
77
  # - With project: exports only that group's keys (no prefix).
78
78
  # - Without project: flat keys as-is, nested keys as GROUP__KEY.
79
- # Keys that aren't valid shell identifiers are silently skipped.
80
- def export_env(project: nil)
79
+ # Keys that aren't valid shell identifiers are skipped. Pass on_skip: callable
80
+ # to be notified (e.g., for warnings).
81
+ def export_env(project: nil, on_skip: nil)
81
82
  secrets = all
82
83
  if project
83
84
  group = secrets[project]
84
85
  return "" unless group.is_a?(Hash)
85
- group.filter_map { |k, v| "export #{k}=#{Shellwords.escape(v.to_s)}" if shell_safe_key?(k) }.join("\n")
86
+ group.filter_map do |k, v|
87
+ if shell_safe_key?(k)
88
+ "export #{k}=#{Shellwords.escape(v.to_s)}"
89
+ else
90
+ on_skip&.call(k)
91
+ nil
92
+ end
93
+ end.join("\n")
86
94
  else
87
95
  secrets.flat_map do |k, v|
88
96
  if v.is_a?(Hash)
89
- next [] unless shell_safe_key?(k)
90
- v.filter_map { |sk, sv| "export #{k.upcase}__#{sk}=#{Shellwords.escape(sv.to_s)}" if shell_safe_key?(sk) }
97
+ unless shell_safe_key?(k)
98
+ on_skip&.call(k)
99
+ next []
100
+ end
101
+ v.filter_map do |sk, sv|
102
+ if shell_safe_key?(sk)
103
+ "export #{k.upcase}__#{sk}=#{Shellwords.escape(sv.to_s)}"
104
+ else
105
+ on_skip&.call("#{k}.#{sk}")
106
+ nil
107
+ end
108
+ end
91
109
  else
92
- shell_safe_key?(k) ? ["export #{k}=#{Shellwords.escape(v.to_s)}"] : []
110
+ if shell_safe_key?(k)
111
+ ["export #{k}=#{Shellwords.escape(v.to_s)}"]
112
+ else
113
+ on_skip&.call(k)
114
+ []
115
+ end
93
116
  end
94
117
  end.join("\n")
95
118
  end
@@ -98,20 +121,40 @@ module LocalVault
98
121
  # Returns a flat hash suitable for env injection.
99
122
  # - With project: only that group's key-value pairs.
100
123
  # - Without project: flat keys + nested keys as GROUP__KEY.
101
- # Keys that aren't valid shell identifiers are silently skipped.
102
- def env_hash(project: nil)
124
+ # Keys that aren't valid shell identifiers are skipped. Pass on_skip: callable
125
+ # to be notified.
126
+ def env_hash(project: nil, on_skip: nil)
103
127
  secrets = all
104
128
  if project
105
129
  group = secrets[project]
106
130
  return {} unless group.is_a?(Hash)
107
- group.each_with_object({}) { |(k, v), h| h[k] = v.to_s if shell_safe_key?(k) }
131
+ group.each_with_object({}) do |(k, v), h|
132
+ if shell_safe_key?(k)
133
+ h[k] = v.to_s
134
+ else
135
+ on_skip&.call(k)
136
+ end
137
+ end
108
138
  else
109
139
  secrets.each_with_object({}) do |(k, v), h|
110
140
  if v.is_a?(Hash)
111
- next unless shell_safe_key?(k)
112
- v.each { |sk, sv| h["#{k.upcase}__#{sk}"] = sv.to_s if shell_safe_key?(sk) }
141
+ unless shell_safe_key?(k)
142
+ on_skip&.call(k)
143
+ next
144
+ end
145
+ v.each do |sk, sv|
146
+ if shell_safe_key?(sk)
147
+ h["#{k.upcase}__#{sk}"] = sv.to_s
148
+ else
149
+ on_skip&.call("#{k}.#{sk}")
150
+ end
151
+ end
113
152
  else
114
- h[k] = v.to_s if shell_safe_key?(k)
153
+ if shell_safe_key?(k)
154
+ h[k] = v.to_s
155
+ else
156
+ on_skip&.call(k)
157
+ end
115
158
  end
116
159
  end
117
160
  end
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.0.3"
2
+ VERSION = "1.0.4"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: localvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.3
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
@@ -41,16 +41,16 @@ dependencies:
41
41
  name: base64
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - ">="
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '0'
46
+ version: '0.2'
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
- - - ">="
51
+ - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '0'
53
+ version: '0.2'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: lipgloss
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -126,7 +126,7 @@ homepage: https://github.com/inventlist/localvault
126
126
  licenses:
127
127
  - MIT
128
128
  metadata:
129
- homepage_uri: https://github.com/inventlist/localvault
129
+ homepage_uri: https://inventlist.com/tools/localvault
130
130
  source_code_uri: https://github.com/inventlist/localvault
131
131
  funding_uri: https://inventlist.com
132
132
  rdoc_options: []