Class: LocalVault::CLI::Team
- Inherits:
-
Thor
- Object
- Thor
- LocalVault::CLI::Team
- Includes:
- TeamHelpers
- Defined in:
- lib/localvault/cli/team.rb
Instance Method Summary collapse
- #add(handle) ⇒ Object
-
#init(vault_name = nil) ⇒ Object
Initialize a vault as a team vault with you as the owner.
-
#list(vault_name = nil) ⇒ Object
List all users who have access to a vault.
- #remove(handle) ⇒ Object
-
#rotate(vault_name = nil) ⇒ Object
Re-key a team vault without adding or removing members.
- #verify(handle) ⇒ Object
Instance Method Details
#add(handle) ⇒ Object
223 224 225 |
# File 'lib/localvault/cli/team.rb', line 223 def add(handle) LocalVault::CLI.new([], , {}).add(handle) end |
#init(vault_name = nil) ⇒ Object
Initialize a vault as a team vault with you as the owner.
This is the explicit transition from personal sync to team-shared sync. Creates the owner’s key slot and bumps the bundle to v3.
Accepts the vault name as either a positional argument (‘team init intellectaco`) or via –vault/-v.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/localvault/cli/team.rb', line 18 def init(vault_name = nil) unless Config.token $stderr.puts "Error: Not logged in." $stderr.puts "\n localvault login YOUR_TOKEN\n" $stderr.puts "Get your token at: https://inventlist.com/@YOUR_HANDLE/edit#developer" return end unless Identity.exists? $stderr.puts "Error: No keypair found. Run: localvault keygen" return end vault_name ||= [:vault] || Config.default_vault handle = Config.inventlist_handle master_key = ensure_master_key(vault_name) return unless master_key client = ApiClient.new(token: Config.token) begin blob = client.pull_vault(vault_name) unless blob.is_a?(String) && !blob.empty? $stderr.puts "Error: Vault '#{vault_name}' has not been synced. Run: localvault sync push -v #{vault_name}" return end data = SyncBundle.unpack(blob) if data[:owner] $stderr.puts "Error: Vault '#{vault_name}' is already a team vault. Owner: @#{data[:owner]}" return end rescue ApiClient::ApiError => e if e.status == 404 $stderr.puts "Error: Vault '#{vault_name}' has not been synced. Run: localvault sync push -v #{vault_name}" else $stderr.puts "Error: #{e.}" end return end # Create owner key slot pub_b64 = Identity.public_key enc_key = KeySlot.create(master_key, pub_b64) key_slots = { handle => { "pub" => pub_b64, "enc_key" => enc_key, "scopes" => nil, "blob" => nil } } # Preserve existing key slots from v2 (upgrade path) data[:key_slots].each do |h, slot| next if h == handle next unless slot.is_a?(Hash) && slot["pub"].is_a?(String) key_slots[h] = slot.merge("scopes" => nil, "blob" => nil) end store = Store.new(vault_name) new_blob = SyncBundle.pack_v3(store, owner: handle, key_slots: key_slots) client.push_vault(vault_name, new_blob) $stdout.puts "Vault '#{vault_name}' is now a team vault." $stdout.puts "Owner: @#{handle}" $stdout.puts "\nNext: localvault add @handle -v #{vault_name}" rescue SyncBundle::UnpackError => e $stderr.puts "Error: #{e.}" end |
#list(vault_name = nil) ⇒ Object
List all users who have access to a vault.
Checks sync-based key slots first; falls back to direct shares if no key slots exist. Displays member handles (key slots) or a share table with ID, recipient, status, and date.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/localvault/cli/team.rb', line 90 def list(vault_name = nil) unless Config.token $stderr.puts "Error: Not logged in." $stderr.puts $stderr.puts " localvault login YOUR_TOKEN" $stderr.puts $stderr.puts "Get your token at: https://inventlist.com/@YOUR_HANDLE/edit#developer" $stderr.puts "New to InventList? Sign up free at https://inventlist.com" $stderr.puts "Docs: https://inventlist.com/sites/localvault/series/localvault" return end vault_name ||= [:vault] || Config.default_vault client = ApiClient.new(token: Config.token) # Try sync-based key slots first key_slots = load_key_slots(client, vault_name) if key_slots && !key_slots.empty? list_key_slots(vault_name, key_slots) return end # Fall back to direct shares result = client.sent_shares(vault_name: vault_name) shares = (result["shares"] || []).reject { |s| s["status"] == "revoked" } if shares.empty? $stdout.puts "No active shares for vault '#{vault_name}'." return end $stdout.puts "Vault: #{vault_name} — #{shares.size} share(s)" $stdout.puts $stdout.printf("%-8s %-20s %-10s %-12s\n", "ID", "Recipient", "Status", "Shared") $stdout.puts("-" * 56) shares.each do |s| date = s["created_at"]&.slice(0, 10) || "" $stdout.printf("%-8s %-20s %-10s %-12s\n", s["id"].to_s, "@#{s["recipient_handle"]}", s["status"], date) end rescue ApiClient::ApiError => e $stderr.puts "Error: #{e.}" end |
#remove(handle) ⇒ Object
231 232 233 |
# File 'lib/localvault/cli/team.rb', line 231 def remove(handle) LocalVault::CLI.new([], , {}).remove(handle) end |
#rotate(vault_name = nil) ⇒ Object
Re-key a team vault without adding or removing members.
Prompts for a new passphrase, re-encrypts all secrets, and rebuilds all key slots. Useful for periodic key rotation.
Accepts the vault name as either a positional argument (‘team rotate intellectaco`) or via –vault/-v.
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/localvault/cli/team.rb', line 143 def rotate(vault_name = nil) unless Config.token $stderr.puts "Error: Not logged in." return end vault_name ||= [:vault] || Config.default_vault client = ApiClient.new(token: Config.token) team_data = load_team_data(client, vault_name) unless team_data && team_data[:key_slots] && !team_data[:key_slots].empty? $stderr.puts "Error: Vault '#{vault_name}' has no team access. Nothing to rotate." return end unless team_data[:owner] $stderr.puts "Error: Vault '#{vault_name}' is not a team vault. Run: localvault team init -v #{vault_name}" return end unless team_data[:owner] == Config.inventlist_handle $stderr.puts "Error: Only the vault owner (@#{team_data[:owner]}) can rotate keys." return end key_slots = team_data[:key_slots] vault_owner = team_data[:owner] master_key = ensure_master_key(vault_name) return unless master_key passphrase = prompt_passphrase("New passphrase for vault '#{vault_name}': ") if passphrase.nil? || passphrase.empty? $stderr.puts "Error: Passphrase cannot be empty." return end # Decrypt existing secrets with the CURRENT master key. After this # point we must not touch the store until the push has succeeded — # see finding #5 (transactional rotate). vault = Vault.new(name: vault_name, master_key: master_key) secrets = vault.all new_salt = Crypto.generate_salt new_master_key = Crypto.derive_master_key(passphrase, new_salt) bundle = build_rotated_bundle( secrets: secrets, key_slots: key_slots, new_master_key: new_master_key, new_salt: new_salt, owner: vault_owner, vault_name: vault_name, vault: vault ) # Push first. If this raises, nothing on local disk has changed, so # the user can safely retry with the same passphrase. client.push_vault(vault_name, bundle[:bundle_json]) # Push succeeded — commit locally and update the session cache. commit_rotated_bundle_locally(vault_name, bundle[:new_secrets_bytes], bundle[:new_meta_bytes]) SessionCache.set(vault_name, new_master_key) $stdout.puts "Vault '#{vault_name}' re-encrypted with new master key." $stdout.puts "#{bundle[:new_slots].size} member(s) updated." rescue ApiClient::ApiError => e $stderr.puts "Error: #{e.}" end |
#verify(handle) ⇒ Object
236 237 238 |
# File 'lib/localvault/cli/team.rb', line 236 def verify(handle) LocalVault::CLI.new([], , {}).verify(handle) end |