localvault 0.9.6

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 671f50a3af0358b5344e1ee94f39cee7b74c7d45c5a55fce9e42da3b1f5b3ffc
4
+ data.tar.gz: a7f41141bff442117b919c5aba3864d51830aedf32ca8ef0ee025bd15bd5ea57
5
+ SHA512:
6
+ metadata.gz: bde2aaed764d7317de96dda2f962a569fe4a9ae9084e14971ee7a0f838d9c2c0a698bf0f3c0ba151f6179b16522ca61a368f5b881b9491d5db7ab74ec8f50e43
7
+ data.tar.gz: dfdeb12993ae4236e98b8b19424b9b03d60f29f842c4586f5f31b4dfeef13e44791e04f0b53d50fb14fb2794bc2939b69eedb71c1029b614a907868a27df0958
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nauman Tariq
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # LocalVault
2
+
3
+ Zero-infrastructure secrets manager. Encrypted secrets stored locally, unlocked with a passphrase.
4
+
5
+ No servers. No cloud. No config files to leak. Just encrypted files on disk.
6
+
7
+ Part of [InventList Tools](https://inventlist.com/tools/localvault) — free, open-source developer utilities for indie builders.
8
+
9
+ ## Install
10
+
11
+ ### Homebrew (macOS)
12
+
13
+ ```bash
14
+ brew install inventlist/tap/localvault
15
+ ```
16
+
17
+ ### RubyGems
18
+
19
+ ```bash
20
+ gem install localvault
21
+ ```
22
+
23
+ **Requires libsodium:**
24
+
25
+ ```bash
26
+ # macOS
27
+ brew install libsodium
28
+
29
+ # Ubuntu/Debian
30
+ sudo apt-get install libsodium-dev
31
+
32
+ # Fedora
33
+ sudo dnf install libsodium-devel
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```bash
39
+ # Create a vault (prompts for passphrase)
40
+ localvault init
41
+
42
+ # Store any sensitive values — API keys, tokens, credentials, database URLs
43
+ localvault set OPENAI_API_KEY "sk-proj-..."
44
+ localvault set STRIPE_SECRET_KEY "sk_live_..."
45
+ localvault set GITHUB_TOKEN "ghp_..."
46
+
47
+ # Retrieve a secret (pipeable)
48
+ localvault get OPENAI_API_KEY
49
+
50
+ # List all keys
51
+ localvault list
52
+
53
+ # 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_..."
58
+
59
+ # Run a command with secrets injected
60
+ localvault exec -- rails server
61
+ localvault exec -- node app.js
62
+ ```
63
+
64
+ ## Commands
65
+
66
+ | Command | Description |
67
+ |---------|-------------|
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 |
72
+ | `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
+ | `version` | Print version |
79
+
80
+ All vault commands accept `--vault NAME` (or `-v NAME`) to target a specific vault. Defaults to `default`.
81
+
82
+ ## Session Caching
83
+
84
+ Avoid typing your passphrase repeatedly:
85
+
86
+ ```bash
87
+ # Unlock once per terminal session
88
+ eval $(localvault unlock)
89
+
90
+ # All subsequent commands skip the passphrase prompt
91
+ localvault get API_KEY
92
+ localvault list
93
+ localvault exec -- rails server
94
+ ```
95
+
96
+ 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.
97
+
98
+ ## Multiple Vaults
99
+
100
+ Separate secrets by project, environment, or service — each vault has its own passphrase and encryption:
101
+
102
+ ```bash
103
+ # Create separate vaults
104
+ localvault init production
105
+ localvault init staging
106
+ localvault init x # all X / Twitter API credentials
107
+
108
+ # Use --vault to target a specific vault
109
+ localvault set API_KEY "sk-prod-xxx" --vault production
110
+ localvault set API_KEY "sk-staging-xxx" --vault staging
111
+
112
+ # Store multiple X accounts in one vault using handle-prefixed keys
113
+ localvault set MYHANDLE_API_KEY "..." --vault x
114
+ localvault set MYHANDLE_API_SECRET "..." --vault x
115
+ localvault set MYHANDLE_ACCESS_TOKEN "..." --vault x
116
+ localvault set MYHANDLE_ACCESS_SECRET "..." --vault x
117
+ localvault set MYHANDLE_BEARER_TOKEN "..." --vault x
118
+
119
+ localvault set MYBRAND_API_KEY "..." --vault x
120
+ localvault set MYBRAND_ACCESS_TOKEN "..." --vault x
121
+
122
+ # List all vaults
123
+ localvault vaults
124
+ # => default (default)
125
+ # => production
126
+ # => staging
127
+ # => x
128
+
129
+ # Unlock a specific vault for a session
130
+ eval $(localvault unlock --vault x)
131
+ localvault exec --vault x -- ruby scripts/post.rb
132
+ ```
133
+
134
+ ## Resetting a Vault
135
+
136
+ Forgot your passphrase? Use `reset` to destroy all secrets and start fresh with a new one:
137
+
138
+ ```bash
139
+ localvault reset
140
+ # WARNING: This will permanently delete all secrets in vault 'default'.
141
+ # This cannot be undone.
142
+ # Type 'default' to confirm: default
143
+ # New passphrase:
144
+ # Confirm passphrase:
145
+ # Vault 'default' has been reset.
146
+ ```
147
+
148
+ Works on named vaults too: `localvault reset production`. All secrets are gone — there is no recovery.
149
+
150
+ ## MCP Server (AI Agents)
151
+
152
+ LocalVault includes an MCP server so AI coding agents can read and manage secrets via the Model Context Protocol — without ever seeing your passphrase.
153
+
154
+ ```bash
155
+ # Unlock your vault first
156
+ eval $(localvault unlock)
157
+ ```
158
+
159
+ Then add to your MCP config (`.mcp.json`, `.cursor/mcp.json`, etc.):
160
+
161
+ ```json
162
+ {
163
+ "mcpServers": {
164
+ "localvault": {
165
+ "command": "localvault",
166
+ "args": ["mcp"],
167
+ "env": {
168
+ "LOCALVAULT_SESSION": "<your-session-token>"
169
+ }
170
+ }
171
+ }
172
+ }
173
+ ```
174
+
175
+ If you've already run `eval $(localvault unlock)` in your terminal, the agent inherits the session automatically — no need to paste the token.
176
+
177
+ **Available tools:** `get_secret`, `list_secrets`, `set_secret`, `delete_secret`
178
+
179
+ See [MCP Setup Guide](docs/site-docs/mcp-setup.md) for Claude Code and Cursor configuration details.
180
+
181
+ ## Security
182
+
183
+ ### Crypto Stack
184
+
185
+ | Layer | Algorithm | Purpose |
186
+ |-------|-----------|---------|
187
+ | Key derivation | **Argon2id** (64 MB, 2 iterations) | Passphrase to master key |
188
+ | Encryption | **XSalsa20-Poly1305** | Authenticated encryption of secrets |
189
+ | Key exchange | **X25519** | Future: shared vaults |
190
+
191
+ - Every encryption uses a random 24-byte nonce
192
+ - Authentication tag prevents tampering (Poly1305)
193
+ - Argon2id is memory-hard, resistant to GPU/ASIC attacks
194
+ - All crypto via [libsodium](https://doc.libsodium.org/) (RbNaCl bindings)
195
+
196
+ ### Storage Layout
197
+
198
+ ```
199
+ ~/.localvault/
200
+ ├── config.yml # Default vault name
201
+ ├── vaults/
202
+ │ ├── default/
203
+ │ │ ├── meta.yml # Salt, creation date, version
204
+ │ │ └── secrets.enc # Encrypted JSON blob
205
+ │ └── production/
206
+ │ ├── meta.yml
207
+ │ └── secrets.enc
208
+ └── keys/ # Future: shared vault keys
209
+ ```
210
+
211
+ - Secrets are stored as a single encrypted JSON blob per vault
212
+ - Atomic writes (temp file + rename) prevent corruption
213
+ - Salt is stored in plaintext metadata (this is standard and safe)
214
+ - The master key is never written to disk
215
+
216
+ ## Development
217
+
218
+ ```bash
219
+ git clone https://github.com/inventlist/localvault.git
220
+ cd localvault
221
+ bundle install
222
+ bundle exec rake test
223
+ ```
224
+
225
+ ## License
226
+
227
+ MIT
data/bin/localvault ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Support both development (require_relative) and installed (load path) usage
4
+ begin
5
+ require_relative "../lib/localvault"
6
+ require_relative "../lib/localvault/cli"
7
+ rescue LoadError
8
+ require "localvault"
9
+ require "localvault/cli"
10
+ end
11
+
12
+ LocalVault::CLI.start(ARGV)
@@ -0,0 +1,125 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module LocalVault
6
+ class ApiClient
7
+ class ApiError < StandardError
8
+ attr_reader :status
9
+
10
+ def initialize(msg, status: nil)
11
+ super(msg)
12
+ @status = status
13
+ end
14
+ end
15
+
16
+ BASE_PATH = "/api/v1"
17
+
18
+ def initialize(token:, base_url: nil)
19
+ @token = token
20
+ @base_url = base_url || Config.api_url
21
+ end
22
+
23
+ # GET /api/v1/users/:handle/public_key
24
+ def get_public_key(handle)
25
+ get("/users/#{handle}/public_key")
26
+ end
27
+
28
+ # PUT /api/v1/profile/public_key
29
+ def publish_public_key(public_key_b64)
30
+ put("/profile/public_key", { public_key: public_key_b64 })
31
+ end
32
+
33
+ # GET /api/v1/vault_shares/pending
34
+ def pending_shares
35
+ get("/vault_shares/pending")
36
+ end
37
+
38
+ # GET /api/v1/vault_shares/sent?vault_name=NAME
39
+ def sent_shares(vault_name: nil)
40
+ path = "/vault_shares/sent"
41
+ path += "?vault_name=#{URI.encode_uri_component(vault_name)}" if vault_name
42
+ get(path)
43
+ end
44
+
45
+ # POST /api/v1/vault_shares
46
+ def create_share(vault_name:, recipient_handle:, encrypted_payload:)
47
+ post("/vault_shares", {
48
+ vault_name: vault_name,
49
+ recipient_handle: recipient_handle,
50
+ encrypted_payload: encrypted_payload
51
+ })
52
+ end
53
+
54
+ # PATCH /api/v1/vault_shares/:id/accept
55
+ def accept_share(id)
56
+ patch("/vault_shares/#{id}/accept", {})
57
+ end
58
+
59
+ # DELETE /api/v1/vault_shares/:id
60
+ def revoke_share(id)
61
+ delete("/vault_shares/#{id}")
62
+ end
63
+
64
+ # GET /api/v1/teams/:handle/members/public_keys
65
+ def team_public_keys(team_handle)
66
+ get("/teams/#{team_handle}/members/public_keys")
67
+ end
68
+
69
+ # GET /api/v1/sites/:slug/crew/public_keys
70
+ def crew_public_keys(site_slug)
71
+ get("/sites/#{site_slug}/crew/public_keys")
72
+ end
73
+
74
+ private
75
+
76
+ def get(path)
77
+ request(:get, path)
78
+ end
79
+
80
+ def post(path, body)
81
+ request(:post, path, body)
82
+ end
83
+
84
+ def put(path, body)
85
+ request(:put, path, body)
86
+ end
87
+
88
+ def patch(path, body = nil)
89
+ request(:patch, path, body)
90
+ end
91
+
92
+ def delete(path)
93
+ request(:delete, path)
94
+ end
95
+
96
+ def request(method, path, body = nil)
97
+ uri = URI("#{@base_url}#{BASE_PATH}#{path}")
98
+ http = Net::HTTP.new(uri.host, uri.port)
99
+ http.use_ssl = uri.scheme == "https"
100
+
101
+ req_class = {
102
+ get: Net::HTTP::Get,
103
+ post: Net::HTTP::Post,
104
+ put: Net::HTTP::Put,
105
+ patch: Net::HTTP::Patch,
106
+ delete: Net::HTTP::Delete
107
+ }.fetch(method)
108
+
109
+ req = req_class.new(uri.request_uri)
110
+ req["Authorization"] = "Bearer #{@token}"
111
+ req["Content-Type"] = "application/json"
112
+ req["Accept"] = "application/json"
113
+ req.body = JSON.generate(body) if body
114
+
115
+ res = http.request(req)
116
+ unless res.is_a?(Net::HTTPSuccess)
117
+ err = begin JSON.parse(res.body)["error"] rescue nil end
118
+ raise ApiError.new(err || "HTTP #{res.code}", status: res.code.to_i)
119
+ end
120
+ JSON.parse(res.body)
121
+ rescue Errno::ECONNREFUSED, SocketError => e
122
+ raise ApiError.new("Cannot connect to #{@base_url}: #{e.message}")
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,56 @@
1
+ require "thor"
2
+
3
+ module LocalVault
4
+ class CLI
5
+ class Keys < Thor
6
+ desc "generate", "Generate an X25519 keypair for vault sharing"
7
+ method_option :force, type: :boolean, default: false, desc: "Overwrite existing keypair"
8
+ def generate
9
+ if Identity.exists? && !options[:force]
10
+ $stdout.puts "Keypair already exists at #{Config.keys_path}"
11
+ $stdout.puts "Use --force to overwrite."
12
+ return
13
+ end
14
+
15
+ Config.ensure_directories!
16
+ Identity.generate!(force: options[:force])
17
+ $stdout.puts "Keypair generated:"
18
+ $stdout.puts " Private: #{Identity.priv_key_path}"
19
+ $stdout.puts " Public: #{Identity.pub_key_path}"
20
+ $stdout.puts
21
+ $stdout.puts "Run 'localvault keys publish' to upload your public key to InventList."
22
+ rescue RuntimeError => e
23
+ $stderr.puts "Error: #{e.message}"
24
+ end
25
+
26
+ desc "publish", "Upload your public key to InventList"
27
+ def publish
28
+ unless Identity.exists?
29
+ $stderr.puts "Error: No keypair found. Run: localvault keys generate"
30
+ return
31
+ end
32
+
33
+ unless Config.token
34
+ $stderr.puts "Error: Not connected. Run: localvault connect --token TOKEN --handle HANDLE"
35
+ return
36
+ end
37
+
38
+ client = ApiClient.new(token: Config.token)
39
+ client.publish_public_key(Identity.public_key)
40
+ $stdout.puts "Public key published to InventList (@#{Config.inventlist_handle})."
41
+ $stdout.puts "Others can now share vaults with you."
42
+ rescue ApiClient::ApiError => e
43
+ $stderr.puts "Error: #{e.message}"
44
+ end
45
+
46
+ desc "show", "Display your public key"
47
+ def show
48
+ unless Identity.exists?
49
+ $stderr.puts "Error: No keypair found. Run: localvault keys generate"
50
+ return
51
+ end
52
+ $stdout.puts Identity.public_key
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ require "thor"
2
+
3
+ module LocalVault
4
+ class CLI
5
+ class Team < Thor
6
+ desc "list [VAULT]", "Show who has access to a vault"
7
+ def list(vault_name = nil)
8
+ unless Config.token
9
+ $stderr.puts "Error: Not connected. Run: localvault connect --token TOKEN --handle HANDLE"
10
+ return
11
+ end
12
+
13
+ vault_name ||= Config.default_vault
14
+ client = ApiClient.new(token: Config.token)
15
+ result = client.sent_shares(vault_name: vault_name)
16
+ shares = (result["shares"] || []).reject { |s| s["status"] == "revoked" }
17
+
18
+ if shares.empty?
19
+ $stdout.puts "No active shares for vault '#{vault_name}'."
20
+ return
21
+ end
22
+
23
+ $stdout.puts "Vault: #{vault_name} — #{shares.size} share(s)"
24
+ $stdout.puts
25
+ $stdout.printf("%-8s %-20s %-10s %-12s\n", "ID", "Recipient", "Status", "Shared")
26
+ $stdout.puts("-" * 56)
27
+ shares.each do |s|
28
+ date = s["created_at"]&.slice(0, 10) || ""
29
+ $stdout.printf("%-8s %-20s %-10s %-12s\n",
30
+ s["id"].to_s, "@#{s["recipient_handle"]}", s["status"], date)
31
+ end
32
+ rescue ApiClient::ApiError => e
33
+ $stderr.puts "Error: #{e.message}"
34
+ end
35
+ end
36
+ end
37
+ end