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 +4 -4
- data/README.md +1 -1
- data/lib/localvault/api_client.rb +9 -6
- data/lib/localvault/cli/sync.rb +3 -1
- data/lib/localvault/cli.rb +4 -2
- data/lib/localvault/config.rb +1 -1
- data/lib/localvault/session_cache.rb +4 -1
- data/lib/localvault/sync_bundle.rb +16 -5
- data/lib/localvault/vault.rb +55 -12
- data/lib/localvault/version.rb +1 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20ee40aedd506f5c39aa06297c8bbcd53ea816d81e6843d96b5749e9014e5ef3
|
|
4
|
+
data.tar.gz: 6b8a4cbcc8178b9f173e1152355084886a9b66d086c981699cfafaf2a37e2d7d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
126
|
-
http.read_timeout
|
|
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
|
|
158
|
-
http.read_timeout
|
|
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
|
|
183
|
-
http.read_timeout
|
|
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)
|
data/lib/localvault/cli/sync.rb
CHANGED
|
@@ -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."
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -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
|
-
$
|
|
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
|
-
|
|
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
|
|
data/lib/localvault/config.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
data/lib/localvault/vault.rb
CHANGED
|
@@ -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
|
|
80
|
-
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
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)
|
|
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
|
|
102
|
-
|
|
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({})
|
|
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
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
data/lib/localvault/version.rb
CHANGED
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.
|
|
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://
|
|
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: []
|