localvault 1.9.1 → 1.10.0

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: 20aa094af163dfa0173fae2eabfc274da78badf84ebc37366bf85163f7b7f77d
4
- data.tar.gz: f113382f8edb4a630732b56de9abe163470c0c377bbcbf64951fde108f6d5e0b
3
+ metadata.gz: 89b873121ec4642cd62fc4b8e7d18fb5bf4dd7f456d0b25b9690a154d3392d51
4
+ data.tar.gz: 90915ed8508f056077cc64ed5c06e9d6925f9c893c70daaf71727fcf4c5d852c
5
5
  SHA512:
6
- metadata.gz: e65ac37ab002da71e42ed5d1e31369dd492982683e2817165060962777e197b8873b6c73c11761d2f7aa829c1c6729f7334691d83d16f0de858c59de2a8abbec
7
- data.tar.gz: 2efaf71acba0f24beaa48313a88d0a9f58058d42f7c80a5ef439af76f29deac6df30435dd74868d04d1bbedf224e1d87499a647e32509cecf0895d696f453934
6
+ metadata.gz: dfcff8362dfd3ec7cbfa25fa4f8b0c599fe6584eedcea0c53f8e6515b028bfada2576cdcf7ef03e1ac130a083b4a869fbb5fe3ec282ab909a9a6f72a0affcdbb
7
+ data.tar.gz: 85ba5c571866b4aa78ad40394d8ffcc3aedfaf6c8c9b5b1013042d1bc3e63ee5d9deb324e0fe8b52ddea8df7026cfa107d1e2b076b9cbf3e741d74d703d8f7aa
data/README.md CHANGED
@@ -88,11 +88,11 @@ localvault exec -- rails server
88
88
  | `init [NAME]` | Create a vault (Argon2id key derivation) |
89
89
  | `set KEY --stdin` | Store a secret from stdin without argv/history exposure |
90
90
  | `set KEY [VALUE]` | Store a secret (positional value kept for compatibility) |
91
- | `set --group GROUP KEY --stdin` | Store a secret in a named group from stdin |
91
+ | `set --group GROUP KEY --stdin` | Store a secret in a named group from stdin (`-g`) |
92
92
  | `get KEY` | Retrieve a secret (raw, pipeable) |
93
93
  | `show` | Display all secrets in a table (masked by default) |
94
- | `show --reveal` | Display with values visible |
95
- | `show --group` | Group by dot-notation prefix (one table per project) |
94
+ | `show --reveal` | Display with values visible (`-r`) |
95
+ | `show --group` | Group by dot-notation prefix, one table per project (`-g`) |
96
96
  | `show --group QUERY` | Show one exact or uniquely matching group |
97
97
  | `groups [QUERY]` | List/search group names and key counts without values |
98
98
  | `list` | List key names only |
@@ -0,0 +1,55 @@
1
+ require "thor"
2
+
3
+ module LocalVault
4
+ class CLI
5
+ class IdentityCommand < Thor
6
+ ALIAS_PATTERN = /\A[a-zA-Z0-9][a-zA-Z0-9_-]{0,31}\z/
7
+
8
+ desc "show", "Show your identity — alias, InventList handle, public key"
9
+ # Print the local identity summary: the configured alias, the
10
+ # InventList handle (if logged in), and the sync public key (if any).
11
+ def show
12
+ $stdout.puts "Alias: #{Config.identity_alias || "-"}"
13
+ $stdout.puts "Handle: #{Config.inventlist_handle ? "@#{Config.inventlist_handle}" : "-"}"
14
+ $stdout.puts "Public key: #{Identity.exists? ? Identity.public_key : "- (run: localvault keygen)"}"
15
+ end
16
+ default_task :show
17
+
18
+ desc "set FIELD VALUE", "Set an identity field (currently: alias)"
19
+ long_desc <<~DESC
20
+ Set a local identity field.
21
+
22
+ SET YOUR ALIAS (a local display name, independent of InventList login):
23
+ \x05 localvault identity set alias nauman
24
+
25
+ The alias is stored in ~/.localvault/config.yml and never leaves
26
+ your machine.
27
+ DESC
28
+ # Set an identity field. Only +alias+ is supported today.
29
+ def set(field, value)
30
+ unless field == "alias"
31
+ $stderr.puts "Error: Unknown field '#{field}'. Supported: alias"
32
+ exit 1
33
+ end
34
+ unless value.match?(ALIAS_PATTERN)
35
+ $stderr.puts "Error: Alias must be 1-32 characters: letters, digits, '-' or '_', starting with a letter or digit."
36
+ exit 1
37
+ end
38
+ Config.identity_alias = value
39
+ $stdout.puts "Alias set to '#{value}'."
40
+ end
41
+
42
+ desc "unset FIELD", "Clear an identity field (currently: alias)"
43
+ # Clear an identity field. Only +alias+ is supported today.
44
+ def unset(field)
45
+ unless field == "alias"
46
+ $stderr.puts "Error: Unknown field '#{field}'. Supported: alias"
47
+ exit 1
48
+ end
49
+ previous = Config.identity_alias
50
+ Config.identity_alias = nil
51
+ $stdout.puts(previous ? "Alias '#{previous}' cleared." : "No alias set.")
52
+ end
53
+ end
54
+ end
55
+ end
@@ -108,87 +108,47 @@ module LocalVault
108
108
 
109
109
  class_option :vault, aliases: "-v", type: :string, desc: "Vault name"
110
110
 
111
+ # Main help = Thor's auto-generated command list (always complete, so new
112
+ # commands appear without touching this file) + a handwritten guide for
113
+ # the concepts and workflows Thor can't derive from command descriptions.
111
114
  def self.help(shell, subcommand = false)
112
115
  shell.say ""
113
116
  shell.say "LocalVault — encrypted local secrets vault with MCP support for AI agents"
114
117
  shell.say " https://inventlist.com/tools/localvault"
115
118
  shell.say ""
119
+ super
120
+ shell.say ""
116
121
  shell.say "GETTING STARTED"
117
122
  shell.say " localvault login [TOKEN] Log in to InventList (enables sync + team features)"
118
123
  shell.say " localvault init [NAME] Create a new encrypted vault"
119
124
  shell.say " localvault demo Create a demo vault to explore commands"
120
125
  shell.say ""
121
- shell.say "SECRETS"
126
+ shell.say "SAFE SECRET INPUT (preferred over passing values as arguments)"
122
127
  shell.say " printf '%s' \"$SECRET\" | localvault set KEY --stdin"
123
- shell.say " Store a secret without argv/history exposure"
124
- shell.say " localvault set KEY VALUE Store a secret (compatibility path)"
125
- shell.say " localvault set --group G K V Store a secret inside group G"
126
- shell.say " localvault get KEY Retrieve a secret"
127
- shell.say " localvault show Display all secrets (masked by default)"
128
- shell.say " localvault groups [QUERY] List or search stored groups (names only)"
129
- shell.say " localvault list List secret key names"
130
- shell.say " localvault delete KEY Remove a secret"
131
- shell.say " localvault import FILE Bulk-import from .env / .json / .yml"
132
- shell.say " localvault env Export as shell variable assignments"
133
- shell.say " localvault exec -- CMD Run a command with secrets injected as env vars"
134
- shell.say ""
135
- shell.say " Use with any CLI: localvault exec -- inventlist ships list"
136
- shell.say " localvault exec -- curl -H \"Authorization: Bearer $API_KEY\" ..."
128
+ shell.say " Store a secret without argv/history exposure"
137
129
  shell.say ""
138
- shell.say "VAULT MANAGEMENT"
139
- shell.say " localvault vaults List all vaults"
140
- shell.say " localvault switch [VAULT] Switch default vault"
141
- shell.say " localvault rekey [NAME] Change vault passphrase"
142
- shell.say " localvault unlock Cache passphrase for session"
143
- shell.say " localvault lock [NAME] Clear cached passphrase"
144
- shell.say " localvault reset [NAME] Destroy and reinitialize a vault"
145
- shell.say " localvault rename OLD NEW Rename a secret key"
146
- shell.say " localvault copy KEY --to V Copy a secret to another vault"
130
+ shell.say "PROJECTS (dot-notation groups inside one vault)"
131
+ shell.say " localvault set platepose.API_KEY v Store a key in the 'platepose' group"
132
+ shell.say " Filter any command with -p: show -p platepose, exec -p platepose -- CMD"
133
+ shell.say " Delete a whole group: localvault delete 'platepose.*'"
147
134
  shell.say ""
148
- shell.say "SYNC (requires localvault login)"
149
- shell.say " localvault sync Sync all vaults bidirectionally (smart push/pull with conflict detection)"
150
- shell.say " localvault sync --dry-run Show what would happen without making changes"
151
- shell.say " localvault sync push [NAME] Push one vault to cloud"
152
- shell.say " localvault sync pull [NAME] Pull one vault from cloud"
153
- shell.say " localvault sync status Show sync status for all vaults"
135
+ shell.say "USING SECRETS WITH ANY CLI"
136
+ shell.say " localvault exec -- inventlist ships list"
137
+ shell.say " localvault exec -- curl -H \"Authorization: Bearer $API_KEY\" ..."
154
138
  shell.say ""
155
139
  shell.say "TEAM SHARING (requires localvault login)"
156
- shell.say " localvault dashboard Aggregate view: owned vaults, vaults shared with you, legacy shares"
157
- shell.say " localvault verify @HANDLE Check if a person has a published public key"
158
- shell.say " localvault add @HANDLE Add teammate (use --scope KEY... for partial access)"
159
- shell.say " localvault remove @HANDLE Remove teammate (--scope KEY to strip one key, --rotate to re-key)"
160
- shell.say " localvault team init [VAULT] Convert vault to team vault (required before add)"
161
- shell.say " localvault team list [VAULT] List vault members and their access"
162
- shell.say " localvault team rotate [VAULT] Re-key vault, keep all members"
163
- shell.say " localvault team SUBCOMMAND See `localvault help team` for the full team namespace"
164
- shell.say " (also accepts `team add/remove/verify` aliases for the top-level commands)"
165
- shell.say ""
166
- shell.say "KEYS (X25519 identity for vault sharing)"
167
- shell.say " localvault keys generate Generate X25519 identity keypair"
168
- shell.say " localvault keys publish Publish public key so others can share vaults with you"
169
- shell.say " localvault keys show Display your current public key"
170
- shell.say " localvault keys SUBCOMMAND See `localvault help keys` for the full keys namespace"
140
+ shell.say " Convert with `team init`, then `add @HANDLE` / `remove @HANDLE`."
141
+ shell.say " Use --scope KEY... for partial access; `team rotate` re-keys for all members."
142
+ shell.say " `team add/remove/verify` also work as aliases for the top-level commands."
171
143
  shell.say ""
172
144
  shell.say "AI / MCP"
173
- shell.say " localvault install-mcp Configure MCP server in your AI tool"
174
- shell.say " localvault mcp Start MCP server (stdio)"
175
- shell.say " localvault mcp --check Check setup and active-vault readiness"
176
- shell.say " Agents: list names, then use localvault_build_exec for safe injection"
177
- shell.say " localvault guard install Block secrets in agent commands (Claude Code hooks)"
178
- shell.say " localvault guard status Show guard hook installation state"
145
+ shell.say " Agents: list secret names, then use localvault_build_exec for safe injection."
146
+ shell.say " `guard install` blocks plaintext secrets in agent commands (Claude Code hooks)."
179
147
  shell.say ""
180
148
  shell.say "LEGACY SHARING (pre-v1.2 direct share, still works as fallback)"
181
- shell.say " localvault keygen Generate X25519 keypair (same as `keys generate`)"
182
- shell.say " localvault share [VAULT] Share a vault with a user, team, or crew (one-shot copy)"
183
- shell.say " localvault receive Fetch and import vaults shared with you"
184
- shell.say " localvault revoke SHARE_ID Revoke a direct vault share"
149
+ shell.say " keygen / share / receive / revoke — superseded by `keys` + team vaults."
185
150
  shell.say ""
186
- shell.say "OTHER"
187
- shell.say " localvault login --status Show current login status"
188
- shell.say " localvault logout Log out"
189
- shell.say " localvault version Print version"
190
- shell.say " localvault doctor Check install and PATH readiness"
191
- shell.say " localvault help [COMMAND] Full help for any command"
151
+ shell.say "Full help for any command: localvault help COMMAND"
192
152
  shell.say ""
193
153
  end
194
154
 
@@ -243,7 +203,7 @@ module LocalVault
243
203
  Use `localvault show -p platepose -v vault` to view a single project.
244
204
  Use `localvault import` to bulk-load from a .env, .json, or .yml file.
245
205
  DESC
246
- method_option :group, type: :string, desc: "Store KEY and VALUE inside this named group"
206
+ method_option :group, aliases: "-g", type: :string, desc: "Store KEY and VALUE inside this named group"
247
207
  method_option :stdin, type: :boolean, default: false, desc: "Read VALUE from stdin instead of argv"
248
208
  def set(key, value = nil)
249
209
  validate_secret_value_source!(value)
@@ -526,8 +486,8 @@ module LocalVault
526
486
  \x05 localvault show -p platepose -v intellectaco # one project only
527
487
  \x05 localvault show -p platepose -v intellectaco --reveal
528
488
  DESC
529
- method_option :group, type: :string, lazy_default: GROUP_ALL_SENTINEL, desc: "Show all groups or one group by name"
530
- method_option :reveal, type: :boolean, default: false, desc: "Show full values instead of masking"
489
+ method_option :group, aliases: "-g", type: :string, lazy_default: GROUP_ALL_SENTINEL, desc: "Show all groups or one group by name"
490
+ method_option :reveal, aliases: "-r", type: :boolean, default: false, desc: "Show full values instead of masking"
531
491
  method_option :project, aliases: "-p", type: :string, desc: "Show only this project group"
532
492
  def show
533
493
  vault = open_vault!
@@ -768,8 +728,10 @@ module LocalVault
768
728
  require_relative "cli/team"
769
729
  require_relative "cli/sync"
770
730
  require_relative "cli/guard"
731
+ require_relative "cli/identity_cmd"
771
732
 
772
733
  register(Guard, "guard", "guard SUBCOMMAND", "Block plaintext secrets in agent tool traffic (Claude Code hooks)")
734
+ register(IdentityCommand, "identity", "identity SUBCOMMAND", "Show or set your local identity (alias, handle, public key)")
773
735
  register(Keys, "keys", "keys SUBCOMMAND", "Manage your X25519 keypair for vault sharing")
774
736
  register(Team, "team", "team SUBCOMMAND", "Manage vault team access")
775
737
  register(Sync, "sync", "sync SUBCOMMAND", "Sync vaults to InventList cloud")
@@ -122,6 +122,27 @@ module LocalVault
122
122
  save(data)
123
123
  end
124
124
 
125
+ # Read the local identity alias.
126
+ #
127
+ # @return [String, nil] the stored alias, or nil
128
+ def self.identity_alias
129
+ load["identity_alias"]
130
+ end
131
+
132
+ # Set the local identity alias. Pass +nil+ to clear it.
133
+ #
134
+ # @param a [String, nil] the alias to store
135
+ # @return [void]
136
+ def self.identity_alias=(a)
137
+ data = load
138
+ if a.nil?
139
+ data.delete("identity_alias")
140
+ else
141
+ data["identity_alias"] = a
142
+ end
143
+ save(data)
144
+ end
145
+
125
146
  # Read the InventList API base URL.
126
147
  #
127
148
  # @return [String] the API URL, defaults to "https://inventlist.com"
@@ -1,4 +1,5 @@
1
1
  require "json"
2
+ require "yaml"
2
3
  require "digest"
3
4
  require_relative "store"
4
5
  require_relative "session_cache"
@@ -15,6 +16,44 @@ module LocalVault
15
16
  # check gets uninstalled.
16
17
  module Guard
17
18
  MIN_VALUE_LENGTH = 8
19
+
20
+ # Keys the operator has explicitly declared non-secret, from
21
+ # ~/.localvault/config.yml:
22
+ #
23
+ # guard_ignore:
24
+ # - CLOUDFLARE_ASSETS.r2_bucket
25
+ #
26
+ # WHY EXPLICIT, AND NOT INFERRED FROM THE KEY NAME.
27
+ # The first attempt exempted keys whose name *looked* public — anything
28
+ # ending in bucket / region / _id — with a veto list of credential-ish
29
+ # words. An audit killed it in one line: a value stored under `AWS.bucket`
30
+ # would be exempt regardless of what it actually contained, so the fix
31
+ # traded a false positive for a false NEGATIVE in a security tool. The veto
32
+ # list was also unbounded (pem, jwk, seed, mnemonic, salt, otp, bearer,
33
+ # sas, …) and every omission is a silent bypass.
34
+ #
35
+ # A key's name cannot describe its value. Only the operator can say "this
36
+ # one is public", so only the operator may — by naming the exact full key,
37
+ # in a file they control, which is greppable and auditable.
38
+ #
39
+ # The problem being solved is still real: a public identifier stored beside
40
+ # real secrets (a bucket name that happens to be a substring of a project
41
+ # path) denies every command mentioning that path, and a guard that cries
42
+ # wolf gets uninstalled, which protects nothing. The answer is an explicit
43
+ # allowlist, not a clever one.
44
+ def self.ignored_keys(config_path = default_config_path)
45
+ return [] unless File.exist?(config_path)
46
+
47
+ raw = YAML.safe_load(File.read(config_path)) || {}
48
+ Array(raw["guard_ignore"]).map(&:to_s)
49
+ rescue StandardError
50
+ [] # an unreadable config must never widen the exemption
51
+ end
52
+
53
+ def self.default_config_path
54
+ File.join(Dir.home, ".localvault", "config.yml")
55
+ end
56
+
18
57
  HOOK_ENTRYPOINT = "localvault guard hook".freeze
19
58
  # The installed command must fail open on machines where the binary is
20
59
  # old, missing, or broken — otherwise every Bash call errors for users
@@ -55,12 +94,17 @@ module LocalVault
55
94
  end
56
95
 
57
96
  # @return [Array<Match>] stored secret values appearing in +text+
58
- def self.scan(text, secrets = unlocked_secrets)
97
+ def self.scan(text, secrets = unlocked_secrets, ignore: ignored_keys)
59
98
  return [] if text.nil? || text.empty?
60
99
 
100
+ ignored = Array(ignore).map(&:to_s)
101
+
61
102
  secrets.filter_map do |entry|
62
103
  value = entry[:value]
63
104
  next if value.nil? || value.length < MIN_VALUE_LENGTH
105
+ # Exact full-key match only. No prefixes, no suffix inference — an
106
+ # operator opting one key out must never silently opt out its siblings.
107
+ next if ignored.include?(entry[:key].to_s)
64
108
  next unless text.include?(value)
65
109
 
66
110
  Match.new(vault: entry[:vault], key: entry[:key], fingerprint: fingerprint(value))
@@ -1,3 +1,3 @@
1
1
  module LocalVault
2
- VERSION = "1.9.1"
2
+ VERSION = "1.10.0"
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.9.1
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
@@ -110,6 +110,7 @@ files:
110
110
  - lib/localvault/cli.rb
111
111
  - lib/localvault/cli/error_presenter.rb
112
112
  - lib/localvault/cli/guard.rb
113
+ - lib/localvault/cli/identity_cmd.rb
113
114
  - lib/localvault/cli/keys.rb
114
115
  - lib/localvault/cli/sync.rb
115
116
  - lib/localvault/cli/team.rb