localvault 1.2.3 → 1.2.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: 4e30526cd10f0e0e29efe3db5b5c73d8de353cad6ad797d02fe1ac7f626ef7bc
4
- data.tar.gz: e6f53442c299ba9836eddd53de5715a3f14577c2213080bfe2b7eab9f4646f41
3
+ metadata.gz: 27e49afd2aae0e73dc4e78e00d81765f77f0c7870a643db8c3549a99b777f27c
4
+ data.tar.gz: 17cba7049d299d9581dceab6dd9c4a10a55214fafa7c08a51d793b05a39282e1
5
5
  SHA512:
6
- metadata.gz: 9d707292c02dace77c7595a61885273145f524e917a1b79a04670b428e5812cbc59564f74cacad4bd73f9a1d0843d91ad45588b33c5b1cf2fae58840df3e484f
7
- data.tar.gz: 761ac32c6161f3c52d6c51ef7f254b8bd8689739cd7a692fc3ae33f03e014de311514f7753d2470068aca7ef005045f672ea87d386cc06daa0909e136521f325
6
+ metadata.gz: 8b38a8ac1fc8774c0f505c65365e91db5a7ea2b7557a6bf437f43c807969be0c4521619f2eccdbc55c402c34e4f097342014c638d3691b5c5c265a2f6a1e0788
7
+ data.tar.gz: 5d35c1ad8fa8573dcec3f9d6a267dead9024cf72cf92faaba4700ac15238d1c72b08d4c746499d1abe23dab92fd19c17cc6c69fb0667e4e94fc75afd770953ba
@@ -185,7 +185,7 @@ module LocalVault
185
185
  return
186
186
  end
187
187
 
188
- handle = handle.delete_prefix("@")
188
+ target = handle
189
189
  vault_name = options[:vault] || Config.default_vault
190
190
  scope_list = options[:scope]
191
191
 
@@ -210,7 +210,6 @@ module LocalVault
210
210
  return
211
211
  end
212
212
 
213
- # Only owner can add members
214
213
  unless data[:owner] == Config.inventlist_handle
215
214
  $stderr.puts "Error: Only the vault owner (@#{data[:owner]}) can manage team access."
216
215
  return
@@ -218,67 +217,76 @@ module LocalVault
218
217
 
219
218
  key_slots = data[:key_slots].is_a?(Hash) ? data[:key_slots] : {}
220
219
 
221
- # Check if member already has full access
222
- if key_slots.key?(handle) && key_slots[handle].is_a?(Hash) && key_slots[handle]["scopes"].nil?
223
- if scope_list
224
- $stdout.puts "@#{handle} already has full vault access."
225
- return
226
- end
227
- end
228
-
229
- # Fetch recipient's public key
230
- result = client.get_public_key(handle)
231
- pub_key = result["public_key"]
232
- unless pub_key && !pub_key.empty?
233
- $stderr.puts "Error: @#{handle} has no public key published."
220
+ # Resolve recipients single @handle, team:HANDLE, or crew:SLUG
221
+ recipients = resolve_add_recipients(client, target)
222
+ if recipients.empty?
223
+ $stderr.puts "Error: No recipients with public keys found for '#{target}'"
234
224
  return
235
225
  end
236
226
 
237
- if scope_list
238
- # Accumulate scopes if member already has some
239
- existing_scopes = key_slots.dig(handle, "scopes") || []
240
- merged_scopes = (existing_scopes + scope_list).uniq
227
+ added = 0
228
+ recipients.each do |member_handle, pub_key|
229
+ next if member_handle == Config.inventlist_handle # skip self
241
230
 
242
- # Create per-member blob with filtered secrets
243
- vault = Vault.new(name: vault_name, master_key: master_key)
244
- filtered = vault.filter(merged_scopes)
231
+ # Skip if already has full access
232
+ if key_slots.key?(member_handle) && key_slots[member_handle].is_a?(Hash) && key_slots[member_handle]["scopes"].nil?
233
+ $stdout.puts "@#{member_handle} already has full vault access." if scope_list
234
+ next
235
+ end
245
236
 
246
- member_key = RbNaCl::Random.random_bytes(32)
247
- encrypted_blob = Crypto.encrypt(JSON.generate(filtered), member_key)
237
+ if scope_list
238
+ existing_scopes = key_slots.dig(member_handle, "scopes") || []
239
+ merged_scopes = (existing_scopes + scope_list).uniq
248
240
 
249
- begin
250
- enc_key = KeySlot.create(member_key, pub_key)
251
- rescue ArgumentError, KeySlot::DecryptionError => e
252
- $stderr.puts "Error: @#{handle}'s public key is invalid: #{e.message}"
253
- return
254
- end
241
+ vault = Vault.new(name: vault_name, master_key: master_key)
242
+ filtered = vault.filter(merged_scopes)
255
243
 
256
- key_slots[handle] = {
257
- "pub" => pub_key,
258
- "enc_key" => enc_key,
259
- "scopes" => merged_scopes,
260
- "blob" => Base64.strict_encode64(encrypted_blob)
261
- }
262
- else
263
- # Full vault access — encrypt master key directly
264
- begin
265
- enc_key = KeySlot.create(master_key, pub_key)
266
- rescue ArgumentError, KeySlot::DecryptionError => e
267
- $stderr.puts "Error: @#{handle}'s public key is invalid: #{e.message}"
268
- return
244
+ member_key = RbNaCl::Random.random_bytes(32)
245
+ encrypted_blob = Crypto.encrypt(JSON.generate(filtered), member_key)
246
+
247
+ begin
248
+ enc_key = KeySlot.create(member_key, pub_key)
249
+ rescue ArgumentError, KeySlot::DecryptionError => e
250
+ $stderr.puts "Error: @#{member_handle}'s public key is invalid: #{e.message}"
251
+ next
252
+ end
253
+
254
+ key_slots[member_handle] = {
255
+ "pub" => pub_key, "enc_key" => enc_key,
256
+ "scopes" => merged_scopes,
257
+ "blob" => Base64.strict_encode64(encrypted_blob)
258
+ }
259
+ else
260
+ begin
261
+ enc_key = KeySlot.create(master_key, pub_key)
262
+ rescue ArgumentError, KeySlot::DecryptionError => e
263
+ $stderr.puts "Error: @#{member_handle}'s public key is invalid: #{e.message}"
264
+ next
265
+ end
266
+
267
+ key_slots[member_handle] = { "pub" => pub_key, "enc_key" => enc_key, "scopes" => nil, "blob" => nil }
269
268
  end
269
+ added += 1
270
+ end
270
271
 
271
- key_slots[handle] = { "pub" => pub_key, "enc_key" => enc_key, "scopes" => nil, "blob" => nil }
272
+ if added == 0
273
+ $stdout.puts "No new members added."
274
+ return
272
275
  end
273
276
 
274
277
  store = Store.new(vault_name)
275
278
  blob = SyncBundle.pack_v3(store, owner: data[:owner], key_slots: key_slots)
276
279
  client.push_vault(vault_name, blob)
277
280
 
278
- if scope_list
279
- $stdout.puts "Added @#{handle} to vault '#{vault_name}' (scopes: #{key_slots[handle]["scopes"].join(", ")})."
281
+ if recipients.size == 1
282
+ h = recipients.first[0]
283
+ if scope_list
284
+ $stdout.puts "Added @#{h} to vault '#{vault_name}' (scopes: #{key_slots[h]["scopes"].join(", ")})."
285
+ else
286
+ $stdout.puts "Added @#{h} to vault '#{vault_name}'."
287
+ end
280
288
  else
281
- $stdout.puts "Added @#{handle} to vault '#{vault_name}'."
289
+ $stdout.puts "Added #{added} member(s) to vault '#{vault_name}'."
282
290
  end
283
291
  rescue ApiClient::ApiError => e
284
292
  if e.status == 404
@@ -456,6 +464,33 @@ module LocalVault
456
464
  nil
457
465
  end
458
466
 
467
+ # Resolve target into list of [handle, public_key] pairs.
468
+ # Supports @handle, team:HANDLE, and crew:SLUG.
469
+ def resolve_add_recipients(client, target)
470
+ if target.start_with?("team:")
471
+ team_handle = target.delete_prefix("team:")
472
+ result = client.team_public_keys(team_handle)
473
+ (result["members"] || [])
474
+ .select { |m| m["handle"] && m["public_key"] && !m["public_key"].empty? }
475
+ .map { |m| [m["handle"], m["public_key"]] }
476
+ elsif target.start_with?("crew:")
477
+ slug = target.delete_prefix("crew:")
478
+ result = client.crew_public_keys(slug)
479
+ (result["members"] || [])
480
+ .select { |m| m["handle"] && m["public_key"] && !m["public_key"].empty? }
481
+ .map { |m| [m["handle"], m["public_key"]] }
482
+ else
483
+ handle = target.delete_prefix("@")
484
+ result = client.get_public_key(handle)
485
+ pub_key = result["public_key"]
486
+ return [] unless pub_key && !pub_key.empty?
487
+ [[handle, pub_key]]
488
+ end
489
+ rescue ApiClient::ApiError => e
490
+ $stderr.puts "Warning: #{e.message}"
491
+ []
492
+ end
493
+
459
494
  # Remove a member's key slot, optionally rotating the vault master key.
460
495
  # Supports partial scope removal via remove_scopes.
461
496
  def remove_key_slot(handle, vault_name, key_slots, client, rotate: false, remove_scopes: nil, owner: nil)
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.2.3"
2
+ VERSION = "1.2.4"
3
3
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: localvault
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.3
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2026-04-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: thor
@@ -131,7 +130,6 @@ metadata:
131
130
  homepage_uri: https://inventlist.com/tools/localvault
132
131
  source_code_uri: https://github.com/inventlist/localvault
133
132
  funding_uri: https://inventlist.com
134
- post_install_message:
135
133
  rdoc_options: []
136
134
  require_paths:
137
135
  - lib
@@ -146,8 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
146
144
  - !ruby/object:Gem::Version
147
145
  version: '0'
148
146
  requirements: []
149
- rubygems_version: 3.0.3.1
150
- signing_key:
147
+ rubygems_version: 3.6.9
151
148
  specification_version: 4
152
149
  summary: Zero-infrastructure secrets manager
153
150
  test_files: []