localvault 0.9.6 → 0.9.7
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 +4 -4
- data/lib/localvault/api_client.rb +69 -0
- data/lib/localvault/cli/sync.rb +111 -0
- data/lib/localvault/cli.rb +83 -1
- data/lib/localvault/sync_bundle.rb +30 -0
- data/lib/localvault/version.rb +1 -1
- data/lib/localvault.rb +1 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c83c59a70defa431aff5c24ddaf567b90363c38faa1498d764e41cb15130e91
|
|
4
|
+
data.tar.gz: 23f6f02e4e17b3fa94e25cdc8266022e2dbe173444381813caed959f4bdd6d04
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 39f17c0a2ab68e9c4ca99d7bdfc92451548da65640fafdf02fad4dcb50dd7105d42d1861f46183a8928a1159d1e0cb24a2f0dca120f359ed384f5785c77f1375
|
|
7
|
+
data.tar.gz: b34099c10e14a8547ef3cca0953310e36995b082e308898b01fedfc710c487628f8e1d96d9f6e65adee33d7f832d77042f52a52f3dc4697b10148b427ed23daa
|
|
@@ -20,6 +20,11 @@ module LocalVault
|
|
|
20
20
|
@base_url = base_url || Config.api_url
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# GET /api/v1/me
|
|
24
|
+
def me
|
|
25
|
+
get("/me")
|
|
26
|
+
end
|
|
27
|
+
|
|
23
28
|
# GET /api/v1/users/:handle/public_key
|
|
24
29
|
def get_public_key(handle)
|
|
25
30
|
get("/users/#{handle}/public_key")
|
|
@@ -71,6 +76,26 @@ module LocalVault
|
|
|
71
76
|
get("/sites/#{site_slug}/crew/public_keys")
|
|
72
77
|
end
|
|
73
78
|
|
|
79
|
+
# GET /api/v1/vaults
|
|
80
|
+
def list_vaults
|
|
81
|
+
get("/vaults")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# PUT /api/v1/vaults/:name — sends raw binary blob, returns JSON
|
|
85
|
+
def push_vault(name, blob)
|
|
86
|
+
request_binary(:put, "/vaults/#{URI.encode_uri_component(name)}", blob)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# GET /api/v1/vaults/:name — returns raw binary blob
|
|
90
|
+
def pull_vault(name)
|
|
91
|
+
request_raw(:get, "/vaults/#{URI.encode_uri_component(name)}")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# DELETE /api/v1/vaults/:name
|
|
95
|
+
def delete_vault(name)
|
|
96
|
+
delete("/vaults/#{URI.encode_uri_component(name)}")
|
|
97
|
+
end
|
|
98
|
+
|
|
74
99
|
private
|
|
75
100
|
|
|
76
101
|
def get(path)
|
|
@@ -121,5 +146,49 @@ module LocalVault
|
|
|
121
146
|
rescue Errno::ECONNREFUSED, SocketError => e
|
|
122
147
|
raise ApiError.new("Cannot connect to #{@base_url}: #{e.message}")
|
|
123
148
|
end
|
|
149
|
+
|
|
150
|
+
# PUT /api/v1/vaults/:name — sends raw binary body, expects JSON response
|
|
151
|
+
def request_binary(method, path, blob)
|
|
152
|
+
uri = URI("#{@base_url}#{BASE_PATH}#{path}")
|
|
153
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
154
|
+
http.use_ssl = uri.scheme == "https"
|
|
155
|
+
|
|
156
|
+
req_class = { put: Net::HTTP::Put }.fetch(method)
|
|
157
|
+
req = req_class.new(uri.request_uri)
|
|
158
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
159
|
+
req["Content-Type"] = "application/octet-stream"
|
|
160
|
+
req["Accept"] = "application/json"
|
|
161
|
+
req.body = blob
|
|
162
|
+
|
|
163
|
+
res = http.request(req)
|
|
164
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
165
|
+
err = begin JSON.parse(res.body)["error"] rescue nil end
|
|
166
|
+
raise ApiError.new(err || "HTTP #{res.code}", status: res.code.to_i)
|
|
167
|
+
end
|
|
168
|
+
JSON.parse(res.body)
|
|
169
|
+
rescue Errno::ECONNREFUSED, SocketError => e
|
|
170
|
+
raise ApiError.new("Cannot connect to #{@base_url}: #{e.message}")
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# GET /api/v1/vaults/:name — returns raw binary body
|
|
174
|
+
def request_raw(method, path)
|
|
175
|
+
uri = URI("#{@base_url}#{BASE_PATH}#{path}")
|
|
176
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
177
|
+
http.use_ssl = uri.scheme == "https"
|
|
178
|
+
|
|
179
|
+
req_class = { get: Net::HTTP::Get }.fetch(method)
|
|
180
|
+
req = req_class.new(uri.request_uri)
|
|
181
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
182
|
+
req["Accept"] = "application/octet-stream"
|
|
183
|
+
|
|
184
|
+
res = http.request(req)
|
|
185
|
+
unless res.is_a?(Net::HTTPSuccess)
|
|
186
|
+
err = begin JSON.parse(res.body)["error"] rescue nil end
|
|
187
|
+
raise ApiError.new(err || "HTTP #{res.code}", status: res.code.to_i)
|
|
188
|
+
end
|
|
189
|
+
res.body
|
|
190
|
+
rescue Errno::ECONNREFUSED, SocketError => e
|
|
191
|
+
raise ApiError.new("Cannot connect to #{@base_url}: #{e.message}")
|
|
192
|
+
end
|
|
124
193
|
end
|
|
125
194
|
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
require "thor"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
|
|
4
|
+
module LocalVault
|
|
5
|
+
class CLI
|
|
6
|
+
class Sync < Thor
|
|
7
|
+
desc "push [NAME]", "Push a vault to InventList cloud sync"
|
|
8
|
+
def push(vault_name = nil)
|
|
9
|
+
return unless logged_in?
|
|
10
|
+
|
|
11
|
+
vault_name ||= Config.default_vault
|
|
12
|
+
store = Store.new(vault_name)
|
|
13
|
+
|
|
14
|
+
unless store.exists?
|
|
15
|
+
$stderr.puts "Error: Vault '#{vault_name}' does not exist. Run: localvault init #{vault_name}"
|
|
16
|
+
return
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
blob = SyncBundle.pack(store)
|
|
20
|
+
client = ApiClient.new(token: Config.token)
|
|
21
|
+
client.push_vault(vault_name, blob)
|
|
22
|
+
|
|
23
|
+
$stdout.puts "Synced vault '#{vault_name}' (#{blob.bytesize} bytes)"
|
|
24
|
+
rescue ApiClient::ApiError => e
|
|
25
|
+
$stderr.puts "Error: #{e.message}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
desc "pull [NAME]", "Pull a vault from InventList cloud sync"
|
|
29
|
+
method_option :force, type: :boolean, default: false, desc: "Overwrite existing local vault"
|
|
30
|
+
def pull(vault_name = nil)
|
|
31
|
+
return unless logged_in?
|
|
32
|
+
|
|
33
|
+
vault_name ||= Config.default_vault
|
|
34
|
+
store = Store.new(vault_name)
|
|
35
|
+
|
|
36
|
+
if store.exists? && !options[:force]
|
|
37
|
+
$stderr.puts "Error: Vault '#{vault_name}' already exists locally. Use --force to overwrite."
|
|
38
|
+
return
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
client = ApiClient.new(token: Config.token)
|
|
42
|
+
blob = client.pull_vault(vault_name)
|
|
43
|
+
data = SyncBundle.unpack(blob)
|
|
44
|
+
|
|
45
|
+
FileUtils.mkdir_p(store.vault_path)
|
|
46
|
+
File.write(store.meta_path, data[:meta])
|
|
47
|
+
store.write_encrypted(data[:secrets]) unless data[:secrets].empty?
|
|
48
|
+
|
|
49
|
+
$stdout.puts "Pulled vault '#{vault_name}'."
|
|
50
|
+
$stdout.puts "Unlock it with: localvault unlock -v #{vault_name}"
|
|
51
|
+
rescue ApiClient::ApiError => e
|
|
52
|
+
if e.status == 404
|
|
53
|
+
$stderr.puts "Error: Vault '#{vault_name}' not found in cloud."
|
|
54
|
+
else
|
|
55
|
+
$stderr.puts "Error: #{e.message}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "status", "Show sync status for all vaults"
|
|
60
|
+
def status
|
|
61
|
+
return unless logged_in?
|
|
62
|
+
|
|
63
|
+
client = ApiClient.new(token: Config.token)
|
|
64
|
+
result = client.list_vaults
|
|
65
|
+
remote = (result["vaults"] || []).each_with_object({}) { |v, h| h[v["name"]] = v }
|
|
66
|
+
local_set = Store.list_vaults.to_set
|
|
67
|
+
all_names = (remote.keys + local_set.to_a).uniq.sort
|
|
68
|
+
|
|
69
|
+
if all_names.empty?
|
|
70
|
+
$stdout.puts "No vaults found locally or in cloud."
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
rows = all_names.map do |name|
|
|
75
|
+
r = remote[name]
|
|
76
|
+
l_exists = local_set.include?(name)
|
|
77
|
+
row_status = if r && l_exists then "synced"
|
|
78
|
+
elsif r then "remote only"
|
|
79
|
+
else "local only"
|
|
80
|
+
end
|
|
81
|
+
synced_at = r ? (r["synced_at"]&.slice(0, 10) || "—") : "—"
|
|
82
|
+
[name, row_status, synced_at]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
max_name = (["Vault"] + rows.map { |r| r[0] }).map(&:length).max
|
|
86
|
+
max_status = (["Status"] + rows.map { |r| r[1] }).map(&:length).max
|
|
87
|
+
|
|
88
|
+
$stdout.puts "#{"Vault".ljust(max_name)} #{"Status".ljust(max_status)} Synced At"
|
|
89
|
+
$stdout.puts "#{"─" * max_name} #{"─" * max_status} ─────────"
|
|
90
|
+
rows.each do |name, row_status, synced_at|
|
|
91
|
+
$stdout.puts "#{name.ljust(max_name)} #{row_status.ljust(max_status)} #{synced_at}"
|
|
92
|
+
end
|
|
93
|
+
rescue ApiClient::ApiError => e
|
|
94
|
+
$stderr.puts "Error: #{e.message}"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.exit_on_failure?
|
|
98
|
+
true
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def logged_in?
|
|
104
|
+
return true if Config.token
|
|
105
|
+
|
|
106
|
+
$stderr.puts "Error: Not logged in. Run: localvault login TOKEN"
|
|
107
|
+
false
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
data/lib/localvault/cli.rb
CHANGED
|
@@ -421,13 +421,95 @@ module LocalVault
|
|
|
421
421
|
$stdout.puts " localvault exec -- env | grep -E 'DATABASE|REDIS'"
|
|
422
422
|
end
|
|
423
423
|
|
|
424
|
-
# ── Teams / sharing
|
|
424
|
+
# ── Teams / sharing / sync ────────────────────────────────────
|
|
425
425
|
|
|
426
426
|
require_relative "cli/keys"
|
|
427
427
|
require_relative "cli/team"
|
|
428
|
+
require_relative "cli/sync"
|
|
428
429
|
|
|
429
430
|
register(Keys, "keys", "keys SUBCOMMAND", "Manage your X25519 keypair for vault sharing")
|
|
430
431
|
register(Team, "team", "team SUBCOMMAND", "Manage vault team access")
|
|
432
|
+
register(Sync, "sync", "sync SUBCOMMAND", "Sync vaults to InventList cloud")
|
|
433
|
+
|
|
434
|
+
desc "keygen", "Generate your identity keypair for vault sync"
|
|
435
|
+
method_option :force, type: :boolean, default: false, desc: "Overwrite existing keypair"
|
|
436
|
+
method_option :show, type: :boolean, default: false, desc: "Print your existing public key"
|
|
437
|
+
def keygen
|
|
438
|
+
if options[:show]
|
|
439
|
+
unless Identity.exists?
|
|
440
|
+
$stdout.puts "No keypair found. Run: localvault keygen"
|
|
441
|
+
return
|
|
442
|
+
end
|
|
443
|
+
$stdout.puts Identity.public_key
|
|
444
|
+
return
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
if Identity.exists? && !options[:force]
|
|
448
|
+
$stdout.puts "Keypair already exists. Use --force to regenerate."
|
|
449
|
+
return
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
Config.ensure_directories!
|
|
453
|
+
Identity.generate!(force: options[:force])
|
|
454
|
+
$stdout.puts "Keypair generated."
|
|
455
|
+
$stdout.puts "Public key: #{Identity.public_key}"
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
desc "login [TOKEN]", "Log in to InventList — validate token, auto-keygen, publish public key"
|
|
459
|
+
method_option :status, type: :boolean, default: false, desc: "Show current login status"
|
|
460
|
+
def login(token = nil)
|
|
461
|
+
if options[:status]
|
|
462
|
+
handle = Config.inventlist_handle
|
|
463
|
+
if handle
|
|
464
|
+
$stdout.puts "Logged in as @#{handle}"
|
|
465
|
+
else
|
|
466
|
+
$stdout.puts "Not logged in. Run: localvault login TOKEN"
|
|
467
|
+
end
|
|
468
|
+
return
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
unless token
|
|
472
|
+
$stdout.puts "Usage: localvault login TOKEN"
|
|
473
|
+
$stdout.puts "Get your token at: https://inventlist.com/settings"
|
|
474
|
+
return
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
client = ApiClient.new(token: token)
|
|
478
|
+
data = client.me
|
|
479
|
+
handle = data.dig("user", "handle")
|
|
480
|
+
|
|
481
|
+
Config.token = token
|
|
482
|
+
Config.inventlist_handle = handle
|
|
483
|
+
|
|
484
|
+
Config.ensure_directories!
|
|
485
|
+
Identity.generate! unless Identity.exists?
|
|
486
|
+
|
|
487
|
+
client.publish_public_key(Identity.public_key)
|
|
488
|
+
|
|
489
|
+
$stdout.puts "Logged in as @#{handle}"
|
|
490
|
+
$stdout.puts "Public key published to your InventList profile."
|
|
491
|
+
$stdout.puts
|
|
492
|
+
$stdout.puts "Next: localvault sync push # sync your vault to the cloud"
|
|
493
|
+
rescue ApiClient::ApiError => e
|
|
494
|
+
if e.status == 401
|
|
495
|
+
$stdout.puts "Invalid token. Check your token at: https://inventlist.com/settings"
|
|
496
|
+
else
|
|
497
|
+
$stdout.puts "Error connecting to InventList: #{e.message}"
|
|
498
|
+
end
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
desc "logout", "Log out of InventList"
|
|
502
|
+
def logout
|
|
503
|
+
unless Config.token
|
|
504
|
+
$stdout.puts "Not logged in."
|
|
505
|
+
return
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
handle = Config.inventlist_handle
|
|
509
|
+
Config.token = nil
|
|
510
|
+
Config.inventlist_handle = nil
|
|
511
|
+
$stdout.puts "Logged out#{" @#{handle}" if handle}."
|
|
512
|
+
end
|
|
431
513
|
|
|
432
514
|
desc "connect", "Connect to InventList for vault sharing"
|
|
433
515
|
method_option :token, required: true, type: :string, desc: "InventList API token"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "base64"
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module LocalVault
|
|
6
|
+
module SyncBundle
|
|
7
|
+
VERSION = 1
|
|
8
|
+
|
|
9
|
+
# Pack a vault's meta.yml + secrets.enc into a single JSON blob.
|
|
10
|
+
# The secrets.enc is already encrypted — this bundle is opaque to the server.
|
|
11
|
+
def self.pack(store)
|
|
12
|
+
meta_content = File.read(store.meta_path)
|
|
13
|
+
secrets_content = store.read_encrypted || ""
|
|
14
|
+
JSON.generate(
|
|
15
|
+
"version" => VERSION,
|
|
16
|
+
"meta" => Base64.strict_encode64(meta_content),
|
|
17
|
+
"secrets" => Base64.strict_encode64(secrets_content)
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Unpack a blob back into {meta:, secrets:} strings.
|
|
22
|
+
def self.unpack(blob)
|
|
23
|
+
data = JSON.parse(blob)
|
|
24
|
+
{
|
|
25
|
+
meta: Base64.strict_decode64(data.fetch("meta")),
|
|
26
|
+
secrets: Base64.strict_decode64(data.fetch("secrets"))
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/lib/localvault/version.rb
CHANGED
data/lib/localvault.rb
CHANGED
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: 0.9.
|
|
4
|
+
version: 0.9.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Nauman Tariq
|
|
@@ -109,6 +109,7 @@ files:
|
|
|
109
109
|
- lib/localvault/api_client.rb
|
|
110
110
|
- lib/localvault/cli.rb
|
|
111
111
|
- lib/localvault/cli/keys.rb
|
|
112
|
+
- lib/localvault/cli/sync.rb
|
|
112
113
|
- lib/localvault/cli/team.rb
|
|
113
114
|
- lib/localvault/config.rb
|
|
114
115
|
- lib/localvault/crypto.rb
|
|
@@ -118,6 +119,7 @@ files:
|
|
|
118
119
|
- lib/localvault/session_cache.rb
|
|
119
120
|
- lib/localvault/share_crypto.rb
|
|
120
121
|
- lib/localvault/store.rb
|
|
122
|
+
- lib/localvault/sync_bundle.rb
|
|
121
123
|
- lib/localvault/vault.rb
|
|
122
124
|
- lib/localvault/version.rb
|
|
123
125
|
homepage: https://github.com/inventlist/localvault
|