maquina_credentials 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +19 -2
- data/lib/maquina/credentials/cli.rb +56 -5
- data/lib/maquina/credentials.rb +30 -9
- data/lib/maquina_credentials/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 79292e220c6fc74153fc3bc34dd018f8426ea4a4a568cc0957ee4894daa092db
|
|
4
|
+
data.tar.gz: b830667d1faef3b732413ec3817b2c468416a552973b586bbd410d37faa38a38
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a095ae071c46db142a393e9a789c1cc1aeb164ee89d1e04fb7a99a6f0a1ef251684876b28927c5f86c6cc51eb9792309b9ee273bdba1e9996710ebab24347827
|
|
7
|
+
data.tar.gz: 1eebb268b9024ce29db8cb9a3dd1c0405654e32764d5775e76070be337c21f5f8cdb94374fed913aecf7afcbb3efb0e10e4c984af3630aec9b9b850806ce5a63
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2026-06-28
|
|
4
|
+
|
|
5
|
+
- `mcr write` now **merges** the YAML from stdin into the existing credentials
|
|
6
|
+
file instead of replacing the whole file. Existing keys are preserved, nested
|
|
7
|
+
hashes are deep-merged, and scalar values are overwritten.
|
|
8
|
+
- Added `mcr edit`, which decrypts the credentials into a temporary file, opens
|
|
9
|
+
it in `$EDITOR` (or `$VISUAL`), and re-encrypts on save. Editing replaces the
|
|
10
|
+
full document, so removing a key in the editor removes it from the file.
|
|
11
|
+
- Added `Maquina::Credentials.merge` and `Maquina::Credentials.read_all` to the
|
|
12
|
+
Ruby API.
|
|
13
|
+
|
|
3
14
|
## [0.1.0] - 2026-06-24
|
|
4
15
|
|
|
5
16
|
- Initial release
|
data/README.md
CHANGED
|
@@ -58,12 +58,23 @@ Read a value (dot-paths traverse nested keys):
|
|
|
58
58
|
MAQUINA_MASTER_KEY=<key> mcr read database.password
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
Write credentials from piped YAML
|
|
61
|
+
Write credentials from piped YAML. `write` **merges** into the existing file,
|
|
62
|
+
so you can add or update keys without resending the whole document:
|
|
62
63
|
|
|
63
64
|
```sh
|
|
64
65
|
MAQUINA_MASTER_KEY=<key> printf "database:\n password: prod-secret\n" | mcr write
|
|
66
|
+
MAQUINA_MASTER_KEY=<key> printf "gh_token: ghp_xxx\n" | mcr write # database.password is kept
|
|
65
67
|
```
|
|
66
68
|
|
|
69
|
+
Edit the full document interactively in `$EDITOR` (re-encrypted on save):
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
EDITOR=vim MAQUINA_MASTER_KEY=<key> mcr edit
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Unlike `write`, `edit` replaces the file with exactly what you save, so deleting
|
|
76
|
+
a key in the editor removes it from the file.
|
|
77
|
+
|
|
67
78
|
Use a specific encrypted file:
|
|
68
79
|
|
|
69
80
|
```sh
|
|
@@ -99,11 +110,17 @@ credentials = Maquina::Credentials.new
|
|
|
99
110
|
credentials.read("anthropic_key")
|
|
100
111
|
credentials.read("database.password") # dot-path traversal
|
|
101
112
|
|
|
102
|
-
#
|
|
113
|
+
# write replaces the whole file from a full hash:
|
|
103
114
|
Maquina::Credentials.write({
|
|
104
115
|
anthropic_key: "sk-ant-...",
|
|
105
116
|
database: {password: "prod-secret"}
|
|
106
117
|
})
|
|
118
|
+
|
|
119
|
+
# merge deep-merges into whatever is already stored:
|
|
120
|
+
Maquina::Credentials.merge({database: {password: "rotated"}})
|
|
121
|
+
|
|
122
|
+
# read_all returns the full decrypted hash (string keys), or {} when absent:
|
|
123
|
+
Maquina::Credentials.read_all
|
|
107
124
|
```
|
|
108
125
|
|
|
109
126
|
- Missing credential paths return an empty string (`""`), never `nil`.
|
|
@@ -2,20 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
require "optparse"
|
|
4
4
|
require "securerandom"
|
|
5
|
+
require "shellwords"
|
|
6
|
+
require "tempfile"
|
|
5
7
|
require "yaml"
|
|
6
8
|
|
|
7
9
|
module Maquina
|
|
8
10
|
class Credentials
|
|
9
11
|
class CLI
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
EDIT_TEMPLATE = <<~YAML
|
|
13
|
+
# Edit your credentials below as YAML, then save and close.
|
|
14
|
+
# Example:
|
|
15
|
+
# database:
|
|
16
|
+
# password: s3cret
|
|
17
|
+
# gh_token: ghp_xxx
|
|
18
|
+
YAML
|
|
19
|
+
|
|
20
|
+
def self.start(argv, stdout: $stdout, stderr: $stderr, stdin: $stdin, editor: nil)
|
|
21
|
+
new(argv, stdout: stdout, stderr: stderr, stdin: stdin, editor: editor).run
|
|
12
22
|
end
|
|
13
23
|
|
|
14
|
-
def initialize(argv, stdout:, stderr:, stdin:)
|
|
24
|
+
def initialize(argv, stdout:, stderr:, stdin:, editor: nil)
|
|
15
25
|
@argv = argv.dup
|
|
16
26
|
@stdout = stdout
|
|
17
27
|
@stderr = stderr
|
|
18
28
|
@stdin = stdin
|
|
29
|
+
@editor = editor || method(:default_editor)
|
|
19
30
|
@credentials_path = nil
|
|
20
31
|
end
|
|
21
32
|
|
|
@@ -32,6 +43,8 @@ module Maquina
|
|
|
32
43
|
read(@argv[1])
|
|
33
44
|
when "write"
|
|
34
45
|
write
|
|
46
|
+
when "edit"
|
|
47
|
+
edit
|
|
35
48
|
else
|
|
36
49
|
@stderr.puts parser
|
|
37
50
|
command ? 1 : 0
|
|
@@ -61,12 +74,14 @@ module Maquina
|
|
|
61
74
|
mcr generate
|
|
62
75
|
mcr read KEY [--file PATH]
|
|
63
76
|
mcr write [--file PATH] < credentials.yml
|
|
77
|
+
mcr edit [--file PATH]
|
|
64
78
|
|
|
65
79
|
Commands:
|
|
66
80
|
help Show this help text.
|
|
67
81
|
generate Print a new random master key to stdout.
|
|
68
82
|
read Print a credential value to stdout.
|
|
69
|
-
write
|
|
83
|
+
write Merge YAML from stdin into the credentials file.
|
|
84
|
+
edit Open the decrypted credentials in $EDITOR, re-encrypt on save.
|
|
70
85
|
|
|
71
86
|
File selection:
|
|
72
87
|
Use --file PATH to read or write a specific file.
|
|
@@ -76,6 +91,7 @@ module Maquina
|
|
|
76
91
|
mcr generate
|
|
77
92
|
mcr read database.password
|
|
78
93
|
mcr --file /tmp/credentials.yml.enc write < credentials.yml
|
|
94
|
+
mcr edit
|
|
79
95
|
mcr help
|
|
80
96
|
TEXT
|
|
81
97
|
|
|
@@ -114,9 +130,44 @@ module Maquina
|
|
|
114
130
|
return 1
|
|
115
131
|
end
|
|
116
132
|
|
|
117
|
-
Credentials.
|
|
133
|
+
Credentials.merge(hash, credentials_path: @credentials_path)
|
|
118
134
|
0
|
|
119
135
|
end
|
|
136
|
+
|
|
137
|
+
def edit
|
|
138
|
+
current = Credentials.read_all(credentials_path: @credentials_path)
|
|
139
|
+
|
|
140
|
+
Tempfile.create(["mcr-credentials", ".yml"]) do |file|
|
|
141
|
+
file.chmod(0o600)
|
|
142
|
+
file.write(current.empty? ? EDIT_TEMPLATE : YAML.dump(current))
|
|
143
|
+
file.flush
|
|
144
|
+
|
|
145
|
+
return 1 unless @editor.call(file.path)
|
|
146
|
+
|
|
147
|
+
edited = File.read(file.path)
|
|
148
|
+
hash = YAML.safe_load(edited, permitted_classes: [], symbolize_names: false)
|
|
149
|
+
|
|
150
|
+
unless hash.is_a?(Hash)
|
|
151
|
+
@stderr.puts "Edited content must be a YAML hash; no changes saved"
|
|
152
|
+
return 1
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Credentials.write(hash, credentials_path: @credentials_path)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
0
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def default_editor(path)
|
|
162
|
+
command = ENV["EDITOR"] || ENV["VISUAL"]
|
|
163
|
+
|
|
164
|
+
if command.nil? || command.empty?
|
|
165
|
+
@stderr.puts "Set $EDITOR or $VISUAL to use mcr edit"
|
|
166
|
+
return false
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
system(*Shellwords.split(command), path)
|
|
170
|
+
end
|
|
120
171
|
end
|
|
121
172
|
end
|
|
122
173
|
end
|
data/lib/maquina/credentials.rb
CHANGED
|
@@ -33,6 +33,25 @@ module Maquina
|
|
|
33
33
|
File.delete(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def self.merge(hash, credentials_path: nil)
|
|
37
|
+
credentials_path = resolve_credentials_path(credentials_path)
|
|
38
|
+
existing = read_all(credentials_path: credentials_path)
|
|
39
|
+
write(deep_merge(existing, deep_stringify(hash)), credentials_path: credentials_path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.read_all(credentials_path: nil)
|
|
43
|
+
credentials_path = resolve_credentials_path(credentials_path)
|
|
44
|
+
return {} unless File.exist?(credentials_path)
|
|
45
|
+
|
|
46
|
+
decrypted = decrypt(File.read(credentials_path), master_key)
|
|
47
|
+
loaded = YAML.safe_load(decrypted, permitted_classes: [], symbolize_names: false)
|
|
48
|
+
raise DecryptionFailed unless loaded.is_a?(Hash)
|
|
49
|
+
|
|
50
|
+
deep_stringify(loaded)
|
|
51
|
+
rescue Psych::Exception
|
|
52
|
+
raise DecryptionFailed
|
|
53
|
+
end
|
|
54
|
+
|
|
36
55
|
def self.encrypt(payload, raw_key)
|
|
37
56
|
cipher = OpenSSL::Cipher.new(CIPHER)
|
|
38
57
|
cipher.encrypt
|
|
@@ -105,6 +124,16 @@ module Maquina
|
|
|
105
124
|
File.join(Dir.pwd, DEFAULT_CREDENTIALS_PATH)
|
|
106
125
|
end
|
|
107
126
|
|
|
127
|
+
def self.deep_merge(base, override)
|
|
128
|
+
base.merge(override) do |_key, base_value, override_value|
|
|
129
|
+
if base_value.is_a?(Hash) && override_value.is_a?(Hash)
|
|
130
|
+
deep_merge(base_value, override_value)
|
|
131
|
+
else
|
|
132
|
+
override_value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
108
137
|
def self.deep_stringify(obj)
|
|
109
138
|
case obj
|
|
110
139
|
when Hash
|
|
@@ -147,15 +176,7 @@ module Maquina
|
|
|
147
176
|
end
|
|
148
177
|
|
|
149
178
|
def load_credentials
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
decrypted = self.class.decrypt(File.read(credentials_path), self.class.master_key)
|
|
153
|
-
loaded = YAML.safe_load(decrypted, permitted_classes: [], symbolize_names: false)
|
|
154
|
-
raise DecryptionFailed unless loaded.is_a?(Hash)
|
|
155
|
-
|
|
156
|
-
self.class.deep_stringify(loaded)
|
|
157
|
-
rescue Psych::Exception
|
|
158
|
-
raise DecryptionFailed
|
|
179
|
+
self.class.read_all(credentials_path: credentials_path)
|
|
159
180
|
end
|
|
160
181
|
end
|
|
161
182
|
end
|