localvault 1.2.2 → 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: 8b2b620a7747bb01fb98a821780c4cd259be663169f3025f168d23f922ee9ac6
4
- data.tar.gz: ccd95f6f3d9879c72d20fdec0750ca3da73fe4ad2eeae0c374481f1a7d0d370e
3
+ metadata.gz: 27e49afd2aae0e73dc4e78e00d81765f77f0c7870a643db8c3549a99b777f27c
4
+ data.tar.gz: 17cba7049d299d9581dceab6dd9c4a10a55214fafa7c08a51d793b05a39282e1
5
5
  SHA512:
6
- metadata.gz: 491eb77e19cdc140567afaa889068d59c323e1a47377c1e224ec9b853fd4b3c4da96b142376b8665af711ffb711007421ce46d9d433cc2c529806899f06f1b4e
7
- data.tar.gz: 59040f871912eae921cdf533c99a032f1e02a4b4e749f54412256375406db1fbecd8cb9899756bd4ffeb01915e871755be8a7e1de763cb2c2e8fb8dc2ccc4b3c
6
+ metadata.gz: 8b38a8ac1fc8774c0f505c65365e91db5a7ea2b7557a6bf437f43c807969be0c4521619f2eccdbc55c402c34e4f097342014c638d3691b5c5c265a2f6a1e0788
7
+ data.tar.gz: 5d35c1ad8fa8573dcec3f9d6a267dead9024cf72cf92faaba4700ac15238d1c72b08d4c746499d1abe23dab92fd19c17cc6c69fb0667e4e94fc75afd770953ba
data/README.md CHANGED
@@ -1,14 +1,16 @@
1
1
  # LocalVault
2
2
 
3
- Zero-infrastructure secrets manager. Encrypted secrets stored locally, unlocked with a passphrase.
3
+ Encrypted local secrets vault with MCP server for AI agents. Zero infrastructure, zero cloud dependency.
4
4
 
5
- No servers. No cloud. No config files to leak. Just encrypted files on disk.
5
+ > **[Try the interactive demo](https://inventlist.com/tools/localvault/cli)** explore every command in your browser.
6
6
 
7
7
  Part of [InventList Tools](https://inventlist.com/tools/localvault) — free, open-source developer utilities for indie builders.
8
8
 
9
+ ---
10
+
9
11
  ## Install
10
12
 
11
- ### Homebrew (macOS)
13
+ ### Homebrew (macOS / Linux)
12
14
 
13
15
  ```bash
14
16
  brew install inventlist/tap/localvault
@@ -39,206 +41,221 @@ sudo dnf install libsodium-devel
39
41
  # Create a vault (prompts for passphrase)
40
42
  localvault init
41
43
 
42
- # Store any sensitive values — API keys, tokens, credentials, database URLs
44
+ # Store secrets
43
45
  localvault set OPENAI_API_KEY "sk-proj-..."
44
46
  localvault set STRIPE_SECRET_KEY "sk_live_..."
45
- localvault set GITHUB_TOKEN "ghp_..."
47
+ localvault set DATABASE_URL "postgres://localhost/myapp"
46
48
 
47
- # Retrieve a secret (pipeable)
49
+ # Retrieve a secret (raw, pipeable)
48
50
  localvault get OPENAI_API_KEY
49
51
 
50
- # List all keys
51
- localvault list
52
+ # View all secrets (masked by default)
53
+ localvault show
54
+
55
+ # Reveal values
56
+ localvault show --reveal
52
57
 
53
58
  # Export as shell variables
54
- localvault env
55
- # => export GITHUB_TOKEN="ghp_..."
56
- # => export OPENAI_API_KEY="sk-proj-..."
57
- # => export STRIPE_SECRET_KEY="sk_live_..."
59
+ eval $(localvault env)
58
60
 
59
61
  # Run a command with secrets injected
60
62
  localvault exec -- rails server
61
- localvault exec -- node app.js
62
63
  ```
63
64
 
64
65
  ## Commands
65
66
 
67
+ ### Secrets
68
+
66
69
  | Command | Description |
67
70
  |---------|-------------|
68
- | `init [NAME]` | Create a vault (prompts for passphrase with confirmation) |
69
- | `set KEY VALUE` | Store a secret |
70
- | `get KEY` | Retrieve a secret (raw value, pipeable) |
71
- | `list` | List all keys |
71
+ | `init [NAME]` | Create a vault (Argon2id key derivation) |
72
+ | `set KEY VALUE` | Store a secret (supports dot-notation: `project.KEY`) |
73
+ | `get KEY` | Retrieve a secret (raw, pipeable) |
74
+ | `show` | Display all secrets in a table (masked by default) |
75
+ | `show --reveal` | Display with values visible |
76
+ | `show --group` | Group by dot-notation prefix (one table per project) |
77
+ | `list` | List key names only |
72
78
  | `delete KEY` | Remove a secret |
73
- | `env` | Export all secrets as `export KEY="value"` lines |
74
- | `exec -- CMD` | Run a command with all secrets as env vars |
75
- | `vaults` | List all vaults |
76
- | `unlock` | Output a session token for passphrase-free access |
77
- | `reset [NAME]` | Destroy all secrets and reinitialize with a new passphrase |
78
- | `login TOKEN` | Log in to InventList (auto-keygen + publish public key) |
79
- | `sync push` | Push vault to InventList cloud |
80
- | `sync pull` | Pull vault from InventList cloud |
81
- | `sync status` | Show sync state for all vaults |
82
- | `team init` | Initialize a vault as a team vault (sets you as owner) |
83
- | `team add @handle` | Add a teammate (with optional `--scope`) |
84
- | `team remove @handle` | Remove a teammate (with optional `--rotate` or `--scope`) |
85
- | `team list` | Show who has access to a synced vault |
86
- | `team rotate` | Re-encrypt vault with new passphrase (no member changes) |
87
- | `version` | Print version |
79
+ | `rename OLD NEW` | Rename a secret key |
80
+ | `copy KEY --to VAULT` | Copy a secret to another vault |
81
+ | `import FILE` | Bulk-import from .env / .json / .yml |
82
+ | `env` | Export as `export KEY="value"` lines |
83
+ | `exec -- CMD` | Run a command with secrets injected as env vars |
88
84
 
89
- All vault commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Defaults to `default`.
85
+ ### Vault Management
90
86
 
91
- ## Session Caching
92
-
93
- Avoid typing your passphrase repeatedly:
94
-
95
- ```bash
96
- # Unlock once per terminal session
97
- eval $(localvault unlock)
98
-
99
- # All subsequent commands skip the passphrase prompt
100
- localvault get API_KEY
101
- localvault list
102
- localvault exec -- rails server
103
- ```
87
+ | Command | Description |
88
+ |---------|-------------|
89
+ | `vaults` | List all vaults with secret counts |
90
+ | `switch [VAULT]` | Switch default vault |
91
+ | `unlock` | Cache passphrase for the session |
92
+ | `lock [NAME]` | Clear cached passphrase |
93
+ | `rekey [NAME]` | Change vault passphrase (re-encrypts all secrets) |
94
+ | `reset [NAME]` | Destroy and reinitialize a vault |
104
95
 
105
- The session token is stored in `LOCALVAULT_SESSION` and contains the derived master key (base64-encoded). It lives only in your shell's memory and disappears when the terminal closes.
96
+ ### Sync & Login
106
97
 
107
- ## Multiple Vaults
98
+ | Command | Description |
99
+ |---------|-------------|
100
+ | `login [TOKEN]` | Log in to InventList — auto-generates X25519 keypair + publishes public key |
101
+ | `login --status` | Show current login status |
102
+ | `logout` | Clear stored credentials |
103
+ | `sync push [NAME]` | Push encrypted vault to cloud |
104
+ | `sync pull [NAME]` | Pull vault from cloud (auto-unlocks if you have a key slot) |
105
+ | `sync status` | Show sync state for all vaults |
106
+ | `config set server URL` | Point at a custom server (default: inventlist.com) |
108
107
 
109
- Separate secrets by project, environment, or service — each vault has its own passphrase and encryption:
108
+ ### Team Sharing (v1.2.0)
110
109
 
111
- ```bash
112
- # Create separate vaults
113
- localvault init production
114
- localvault init staging
115
- localvault init x # all X / Twitter API credentials
116
-
117
- # Use --vault to target a specific vault
118
- localvault set API_KEY "sk-prod-xxx" --vault production
119
- localvault set API_KEY "sk-staging-xxx" --vault staging
120
-
121
- # Store multiple X accounts in one vault using handle-prefixed keys
122
- localvault set MYHANDLE_API_KEY "..." --vault x
123
- localvault set MYHANDLE_API_SECRET "..." --vault x
124
- localvault set MYHANDLE_ACCESS_TOKEN "..." --vault x
125
- localvault set MYHANDLE_ACCESS_SECRET "..." --vault x
126
- localvault set MYHANDLE_BEARER_TOKEN "..." --vault x
127
-
128
- localvault set MYBRAND_API_KEY "..." --vault x
129
- localvault set MYBRAND_ACCESS_TOKEN "..." --vault x
130
-
131
- # List all vaults
132
- localvault vaults
133
- # => default (default)
134
- # => production
135
- # => staging
136
- # => x
137
-
138
- # Unlock a specific vault for a session
139
- eval $(localvault unlock --vault x)
140
- localvault exec --vault x -- ruby scripts/post.rb
141
- ```
110
+ | Command | Description |
111
+ |---------|-------------|
112
+ | `team init` | Convert vault to team vault (sets you as owner, SyncBundle v3) |
113
+ | `team verify @handle` | Check if a user has a published public key (dry-run) |
114
+ | `team add @handle` | Add teammate with full vault access |
115
+ | `team add @handle --scope KEY...` | Add teammate with access to specific keys only |
116
+ | `team remove @handle` | Remove teammate's access |
117
+ | `team remove @handle --scope KEY` | Remove one scoped key (keeps other scopes) |
118
+ | `team remove @handle --rotate` | Full revocation + re-encrypt with new passphrase |
119
+ | `team list` | List vault members |
120
+ | `team rotate` | Re-key vault with new passphrase, keep all members |
121
+
122
+ ### Keys
142
123
 
143
- ## Resetting a Vault
124
+ | Command | Description |
125
+ |---------|-------------|
126
+ | `keys generate` | Generate X25519 identity keypair |
127
+ | `keys show` | Display your public key |
128
+ | `keys publish` | Upload public key to InventList (required before others can add you) |
144
129
 
145
- Forgot your passphrase? Use `reset` to destroy all secrets and start fresh with a new one:
130
+ ### AI / MCP
146
131
 
147
- ```bash
148
- localvault reset
149
- # WARNING: This will permanently delete all secrets in vault 'default'.
150
- # This cannot be undone.
151
- # Type 'default' to confirm: default
152
- # New passphrase:
153
- # Confirm passphrase:
154
- # Vault 'default' has been reset.
155
- ```
132
+ | Command | Description |
133
+ |---------|-------------|
134
+ | `install-mcp [CLIENT]` | Configure MCP server in claude-code, cursor, windsurf, or zed |
135
+ | `mcp` | Start MCP server (stdio transport) |
156
136
 
157
- Works on named vaults too: `localvault reset production`. All secrets are gone there is no recovery.
137
+ All commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Default vault is `default`.
158
138
 
159
- ## Cloud Sync
139
+ ## Personal Sync
160
140
 
161
- Sync vaults across devices via [InventList](https://inventlist.com). Your secrets stay encrypted — the server never sees plaintext.
141
+ Sync your vaults between machines same passphrase, no team features needed:
162
142
 
163
143
  ```bash
164
- # Log in (auto-generates keypair + publishes public key)
165
- localvault login YOUR_TOKEN
166
-
167
- # Push a vault to the cloud
144
+ # Machine A: push your vault
168
145
  localvault sync push
169
146
 
170
- # Pull on another device
147
+ # Machine B: install, login, pull
148
+ brew install inventlist/tap/localvault
149
+ localvault login YOUR_TOKEN
171
150
  localvault sync pull
151
+ localvault show # enter your passphrase — same secrets
152
+ ```
153
+
154
+ Check what's synced:
172
155
 
173
- # Check sync status
156
+ ```bash
174
157
  localvault sync status
158
+ # default synced 2 minutes ago
159
+ # production local only —
175
160
  ```
176
161
 
177
162
  ## Team Sharing
178
163
 
179
- Share vault access with teammates using X25519 key slots. Three modes: personal sync (just you), direct share (one-time handoff), and team sync (ongoing shared access with scoping).
164
+ Share vault access with teammates using X25519 asymmetric encryption. The server never sees plaintext.
180
165
 
181
166
  ```bash
182
- # Initialize a vault as a team vault (you become the owner)
167
+ # 1. Convert to team vault (required first)
183
168
  localvault team init -v production
184
169
 
185
- # Add a teammate with full access
186
- localvault team add @bob -v production
187
-
188
- # Add a teammate with scoped access (only specific keys)
189
- localvault team add @carol -v production --scope platepose DATABASE_URL
170
+ # 2. Verify teammate has a published key
171
+ localvault team verify @alice
190
172
 
191
- # See who has access
192
- localvault team list -v production
193
- # => @alice (owner)
194
- # => @bob (full vault)
195
- # => @carol (scopes: platepose, DATABASE_URL)
173
+ # 3. Add with full access
174
+ localvault team add @alice -v production
196
175
 
197
- # Remove access
198
- localvault team remove @bob -v production
176
+ # 4. Or scoped — they only see specific keys
177
+ localvault team add @bob -v production --scope STRIPE_KEY WEBHOOK_SECRET
199
178
 
200
- # Remove a specific scope (keeps other scopes)
201
- localvault team remove @carol -v production --scope DATABASE_URL
179
+ # 5. When Alice pulls, auto-unlocks via her identity key
180
+ # (on Alice's machine)
181
+ localvault sync pull production
182
+ # => Unlocked via your identity key.
202
183
 
203
- # Remove + re-encrypt with new passphrase (full revocation)
204
- localvault team remove @bob -v production --rotate
184
+ # 6. Scoped members can't push
185
+ # (on Bob's machine)
186
+ localvault sync push production
187
+ # => Error: You have scoped access. Only the owner can push.
205
188
 
206
- # Re-key without removing anyone (periodic rotation)
189
+ # 7. Rotate without removing anyone
207
190
  localvault team rotate -v production
191
+
192
+ # 8. Full revocation + re-key
193
+ localvault team remove @alice -v production --rotate
208
194
  ```
209
195
 
210
- When a teammate pulls a vault they have a key slot for, it auto-unlocks via their identity key. Scoped members see only their authorized keys they don't know other keys exist.
196
+ **Prerequisites:** Teammates must have a published public key. `localvault login` does this automatically, or: `localvault keys generate && localvault keys publish`.
211
197
 
212
198
  ## MCP Server (AI Agents)
213
199
 
214
- LocalVault includes an MCP server so AI coding agents can read and manage secrets via the Model Context Protocol without ever seeing your passphrase.
200
+ Give AI agents safe secret access. Keys never appear in agent context or config files.
215
201
 
216
202
  ```bash
217
- # Unlock your vault first
218
- eval $(localvault unlock)
203
+ # One-command install for Claude Code
204
+ localvault install-mcp claude-code
205
+ # Also supports: cursor, windsurf, zed
206
+
207
+ # Unlock your vault for the session
208
+ localvault unlock
209
+
210
+ # MCP tools available to the agent:
211
+ # get_secret(key, vault?) — read a secret
212
+ # list_secrets(vault?, prefix?) — list key names
213
+ # set_secret(key, value, vault?) — store a secret
214
+ # delete_secret(key, vault?) — remove a secret
219
215
  ```
220
216
 
221
- Then add to your MCP config (`.mcp.json`, `.cursor/mcp.json`, etc.):
222
-
223
- ```json
224
- {
225
- "mcpServers": {
226
- "localvault": {
227
- "command": "localvault",
228
- "args": ["mcp"],
229
- "env": {
230
- "LOCALVAULT_SESSION": "<your-session-token>"
231
- }
232
- }
233
- }
234
- }
217
+ **exec_action** agent declares intent, LocalVault executes with secrets injected. The agent never sees the key:
218
+
219
+ ```bash
220
+ localvault exec_action -- curl -s https://api.openai.com/v1/models \
221
+ -H "Authorization: Bearer $OPENAI_API_KEY"
235
222
  ```
236
223
 
237
- If you've already run `eval $(localvault unlock)` in your terminal, the agent inherits the session automatically — no need to paste the token.
224
+ ## Multi-Project Vaults
238
225
 
239
- **Available tools:** `get_secret`, `list_secrets`, `set_secret`, `delete_secret`
226
+ One vault, many projects. Dot-notation keeps secrets organized:
240
227
 
241
- See [MCP for AI Agents](https://inventlist.com/sites/localvault/series/localvault/mcp-for-ai-agents) for Claude Code and Cursor configuration details.
228
+ ```bash
229
+ # Store with project prefix
230
+ localvault set myapp.DATABASE_URL postgres://localhost/myapp -v work
231
+ localvault set api.DATABASE_URL postgres://localhost/api -v work
232
+
233
+ # View grouped by project
234
+ localvault show --group -v work
235
+
236
+ # Filter to one project
237
+ localvault show -p myapp -v work
238
+
239
+ # Export one project
240
+ eval $(localvault env -p myapp -v work)
241
+
242
+ # Bulk import
243
+ localvault import .env --prefix myapp -v work
244
+ ```
245
+
246
+ ## Session Caching
247
+
248
+ Avoid typing your passphrase repeatedly:
249
+
250
+ ```bash
251
+ eval $(localvault unlock)
252
+
253
+ # All subsequent commands skip the passphrase prompt
254
+ localvault get API_KEY
255
+ localvault exec -- rails server
256
+ ```
257
+
258
+ Session lives in `LOCALVAULT_SESSION` — disappears when the terminal closes.
242
259
 
243
260
  ## Security
244
261
 
@@ -246,20 +263,23 @@ See [MCP for AI Agents](https://inventlist.com/sites/localvault/series/localvaul
246
263
 
247
264
  | Layer | Algorithm | Purpose |
248
265
  |-------|-----------|---------|
249
- | Key derivation | **Argon2id** (64 MB, 2 iterations) | Passphrase to master key |
250
- | Encryption | **XSalsa20-Poly1305** | Authenticated encryption of secrets |
266
+ | Key derivation | **Argon2id** (64 MB, 3 iterations) | Passphrase master key |
267
+ | Encryption | **XSalsa20-Poly1305** | Authenticated encryption |
251
268
  | Key exchange | **X25519** | Team key slots + vault sharing |
252
269
 
253
- - Every encryption uses a random 24-byte nonce
254
- - Authentication tag prevents tampering (Poly1305)
255
- - Argon2id is memory-hard, resistant to GPU/ASIC attacks
270
+ - Random 24-byte nonce per encryption
271
+ - Poly1305 authentication prevents tampering
272
+ - Argon2id is memory-hard (GPU/ASIC resistant)
256
273
  - All crypto via [libsodium](https://doc.libsodium.org/) (RbNaCl bindings)
274
+ - SyncBundle v3 for team vaults (owner field + per-member key slots)
257
275
 
258
276
  ### Storage Layout
259
277
 
260
278
  ```
261
279
  ~/.localvault/
262
- ├── config.yml # Default vault name
280
+ ├── config.yml # Default vault, server URL, token
281
+ ├── identity.key # X25519 private key (encrypted at rest)
282
+ ├── identity.pub # X25519 public key (safe to share)
263
283
  ├── vaults/
264
284
  │ ├── default/
265
285
  │ │ ├── meta.yml # Salt, creation date, version
@@ -267,13 +287,19 @@ See [MCP for AI Agents](https://inventlist.com/sites/localvault/series/localvaul
267
287
  │ └── production/
268
288
  │ ├── meta.yml
269
289
  │ └── secrets.enc
270
- └── keys/ # X25519 identity keypair for sync + team access
271
290
  ```
272
291
 
273
- - Secrets are stored as a single encrypted JSON blob per vault
274
- - Atomic writes (temp file + rename) prevent corruption
275
- - Salt is stored in plaintext metadata (this is standard and safe)
276
- - The master key is never written to disk
292
+ ## Server Independence
293
+
294
+ LocalVault is server-agnostic. It ships configured for `inventlist.com` but works with any host that implements the protocol (4 endpoints):
295
+
296
+ ```bash
297
+ # Use a different server
298
+ localvault config set server https://vaulthost.example
299
+
300
+ # Or override per-login
301
+ localvault login --server https://vaulthost.example
302
+ ```
277
303
 
278
304
  ## Development
279
305
 
@@ -281,7 +307,7 @@ See [MCP for AI Agents](https://inventlist.com/sites/localvault/series/localvaul
281
307
  git clone https://github.com/inventlist/localvault.git
282
308
  cd localvault
283
309
  bundle install
284
- bundle exec rake test
310
+ bundle exec rake test # 463 tests, 918 assertions
285
311
  ```
286
312
 
287
313
  ## Used by
@@ -290,4 +316,5 @@ Powers credentials management at [InventList](https://inventlist.com) — where
290
316
 
291
317
  ## License
292
318
 
293
- MIT
319
+ Apache 2.0 — see [LICENSE](LICENSE).
320
+ Built by the [InventList](https://inventlist.com) team.
@@ -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)
@@ -26,7 +26,10 @@ module LocalVault
26
26
  shell.say " localvault delete KEY Remove a secret"
27
27
  shell.say " localvault import FILE Bulk-import from .env / .json / .yml"
28
28
  shell.say " localvault env Export as shell variable assignments"
29
- shell.say " localvault exec -- CMD Run a command with secrets injected"
29
+ shell.say " localvault exec -- CMD Run a command with secrets injected as env vars"
30
+ shell.say ""
31
+ shell.say " Use with any CLI: localvault exec -- inventlist ships list"
32
+ shell.say " localvault exec -- curl -H \"Authorization: Bearer $API_KEY\" ..."
30
33
  shell.say ""
31
34
  shell.say "VAULT MANAGEMENT"
32
35
  shell.say " localvault vaults List all vaults"
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.2.2"
2
+ VERSION = "1.2.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.2.2
4
+ version: 1.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq