vaultkit 1.0.6 → 1.0.8

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: c99e29a96bcd6b84bf3de1322b79f256118f5025918ef7573563d66f42ad9b93
4
- data.tar.gz: bb09e5336124994c39127859bc9cb978b86cb6c4d5a2a610080f608f262f4c6a
3
+ metadata.gz: 3cc5f978b053f8fe4ac4e146a50b90e2fecc93b884073816c760bb0d99502b96
4
+ data.tar.gz: 26b63ecee6b659f0bf0f645455963f712aac27df77850e142dabc894d5b8070a
5
5
  SHA512:
6
- metadata.gz: 1899ddef1628cf6d67e394b5d2c7e616e25a6c17e7ee86c609beff0cf02c8339e2611e2b78a55438312a59e3e51389cf5163e55089efc5a4a498548077837f63
7
- data.tar.gz: 0a2b23c898735fc6b706549d8e45872bc976a00bc4750843115f3eaf825b587595a40ed5095b72dc57f8c61e292ef489e478754bd92f5459e57aa97bf5048e73
6
+ metadata.gz: ba3984df812852cab48f7a530d77d021a8ecbb43432cd212f3ed7d0cd14c71705fce6894f05d3b7db84569168cd4004609fe9f9a702ca2b90c6130c59dcf3402
7
+ data.tar.gz: 7d5c1ca12d0fb08798c6c85bfa2fc10fd6db0667129f7d2fa51e72542dae471c734511f3480e1b73ba3c070903985fb5c42bb789905c1dea3aa4e383d3a6b599
@@ -169,13 +169,40 @@ module Vkit
169
169
  )
170
170
  end
171
171
 
172
+ # REGISTRY
173
+ desc "registry SUBCOMMAND ...ARGS", "Manage local registry.yaml"
174
+ subcommand "registry", Class.new(Thor) {
175
+
176
+ desc "export", "Export runtime registry to datasets/registry.yaml"
177
+ option :dir, type: :string, default: "."
178
+ option :out, type: :string, desc: "Custom output path"
179
+ option :force, type: :boolean, default: false
180
+ def export
181
+ Commands::RegistryExportCommand.new.call(
182
+ dir: options[:dir],
183
+ out: options[:out],
184
+ force: options[:force]
185
+ )
186
+ end
187
+
188
+ desc "diff", "Diff local registry.yaml against runtime registry"
189
+ option :dir, type: :string, default: "."
190
+ option :format, type: :string, default: "human", enum: %w[human json]
191
+ def diff
192
+ Commands::RegistryDiffCommand.new.call(
193
+ dir: options[:dir],
194
+ format: options[:format]
195
+ )
196
+ end
197
+ }
198
+
172
199
  # POLICY
173
200
  desc "policy SUBCOMMAND ...ARGS", "Manage policy bundles"
174
201
  subcommand "policy", Class.new(Thor) {
175
202
 
176
203
  desc "bundle", "Compile YAML policies into a JSON policy bundle"
177
204
  option :policies_dir, type: :string, default: "config/policies"
178
- option :registry_dir, type: :string, default: "config"
205
+ option :registry_dir, type: :string, default: "datasets"
179
206
  option :out, type: :string, default: "dist/policy_bundle.json"
180
207
  option :org, type: :string
181
208
  option :version, type: :string
@@ -16,8 +16,7 @@ module Vkit
16
16
  body: {}
17
17
  )
18
18
 
19
- rows = response.dig("rows", "rows") || []
20
- meta = response.dig("rows", "meta") || {}
19
+ rows, meta = normalize_response(response)
21
20
 
22
21
  print_result(rows, meta, format)
23
22
  end
@@ -42,6 +41,24 @@ module Vkit
42
41
  raise "Unknown format: #{format}"
43
42
  end
44
43
  end
44
+
45
+ def normalize_response(response)
46
+ rows =
47
+ if response["rows"].is_a?(Array)
48
+ response["rows"]
49
+ else
50
+ response.dig("rows", "rows") || []
51
+ end
52
+
53
+ meta =
54
+ if response["meta"].is_a?(Hash)
55
+ response["meta"]
56
+ else
57
+ response.dig("rows", "meta") || {}
58
+ end
59
+
60
+ [rows, meta]
61
+ end
45
62
  end
46
63
  end
47
64
  end
@@ -59,15 +59,25 @@ module Vkit
59
59
  FileUtils.mkdir_p(File.join(dir, "datasets"))
60
60
  FileUtils.mkdir_p(File.join(dir, "dist"))
61
61
  FileUtils.mkdir_p(File.join(dir, ".vkit"))
62
-
62
+
63
63
  registry_path = File.join(dir, "datasets", "registry.yaml")
64
- unless File.exist?(registry_path)
64
+
65
+ if File.exist?(registry_path)
66
+ puts "ℹ️ datasets/registry.yaml already exists — leaving unchanged."
67
+ else
65
68
  File.write(
66
69
  registry_path,
67
- "# Dataset registry\n# Populate with: vkit scan <datasource> --apply\n"
70
+ <<~YAML
71
+ # VaultKit Dataset Registry
72
+ #
73
+ # Populate with:
74
+ # vkit scan <datasource> --apply
75
+ # vkit registry export
76
+ #
77
+ YAML
68
78
  )
69
79
  end
70
-
80
+
71
81
  gitignore_path = File.join(dir, ".gitignore")
72
82
  unless File.exist?(gitignore_path)
73
83
  File.write(
@@ -75,7 +85,7 @@ module Vkit
75
85
  "dist/\n.vkit/\n"
76
86
  )
77
87
  end
78
- end
88
+ end
79
89
  end
80
90
  end
81
91
  end
@@ -67,22 +67,21 @@ module Vkit
67
67
  private
68
68
 
69
69
  def git_sha
70
- `git rev-parse HEAD`.strip
71
- rescue
70
+ out = `git rev-parse HEAD 2>/dev/null`.strip
71
+ return out unless out.empty?
72
+
72
73
  Time.now.to_i.to_s
73
74
  end
74
-
75
+
75
76
  def git_repo
76
- `git config --get remote.origin.url`.strip
77
- rescue
78
- nil
77
+ out = `git config --get remote.origin.url 2>/dev/null`.strip
78
+ out.empty? ? nil : out
79
79
  end
80
-
80
+
81
81
  def git_ref
82
- `git rev-parse --abbrev-ref HEAD`.strip
83
- rescue
84
- nil
85
- end
82
+ out = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
83
+ out.empty? ? nil : out
84
+ end
86
85
  end
87
86
  end
88
87
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require_relative "../../core/registry_diff"
6
+ require_relative "../../core/registry_diff_printer"
7
+
8
+ module Vkit
9
+ module CLI
10
+ module Commands
11
+ class RegistryDiffCommand < BaseCommand
12
+ DEFAULT_PATH = File.join("datasets", "registry.yaml")
13
+
14
+ def call(dir:, format: "human")
15
+ ensure_project!(dir)
16
+
17
+ dir = File.expand_path(dir)
18
+ path = File.join(dir, DEFAULT_PATH)
19
+
20
+ unless File.exist?(path)
21
+ puts "❌ No local registry.yaml found."
22
+ exit 2
23
+ end
24
+
25
+ with_auth do
26
+ user = credential_store.user
27
+ org = user["organization_slug"]
28
+
29
+ local = YAML.safe_load(
30
+ File.read(path),
31
+ permitted_classes: [],
32
+ permitted_symbols: [],
33
+ aliases: false
34
+ )
35
+ runtime = authenticated_client.get(
36
+ "/api/v1/orgs/#{org}/registries/export"
37
+ )
38
+
39
+ diff = Vkit::Core::RegistryDiff.compute(
40
+ local: local,
41
+ remote: runtime
42
+ )
43
+
44
+ if format == "json"
45
+ puts JSON.pretty_generate(diff)
46
+ exit diff["datasets"].empty? ? 0 : 1
47
+ else
48
+ Vkit::Core::RegistryDiffPrinter.print(diff)
49
+ exit diff["datasets"].empty? ? 0 : 1
50
+ end
51
+ end
52
+
53
+ rescue StandardError => e
54
+ puts "❌ Error: #{e.message}"
55
+ exit 2
56
+ end
57
+
58
+ private
59
+
60
+ def ensure_project!(dir)
61
+ raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "base_command"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class RegistryExportCommand < BaseCommand
10
+ DEFAULT_PATH = File.join("datasets", "registry.yaml")
11
+
12
+ def call(dir:, out: nil, force: false)
13
+ with_auth do
14
+ ensure_project!(dir)
15
+
16
+ dir = File.expand_path(dir)
17
+ path = out ? File.expand_path(out, dir) : File.join(dir, DEFAULT_PATH)
18
+
19
+ if File.exist?(path) && !force
20
+ decision = handle_existing_registry(dir: dir, path: path)
21
+ return if decision == :abort
22
+ end
23
+
24
+ user = credential_store.user
25
+ org = user["organization_slug"]
26
+
27
+ registry_data = authenticated_client.get(
28
+ "/api/v1/orgs/#{org}/registries/export"
29
+ )
30
+
31
+ FileUtils.mkdir_p(File.dirname(path))
32
+ File.write(path, registry_data.to_yaml)
33
+
34
+ puts "✅ Registry exported to:"
35
+ puts " #{relative(dir, path)}"
36
+ end
37
+ rescue StandardError => e
38
+ puts "❌ Error: #{e.message}"
39
+ exit 1
40
+ end
41
+
42
+ private
43
+
44
+ def ensure_project!(dir)
45
+ raise "Not a VaultKit project (missing .vkit/)" unless Dir.exist?(File.join(dir, ".vkit"))
46
+ end
47
+
48
+ def handle_existing_registry(dir:, path:)
49
+ puts "\n⚠️ Existing registry.yaml detected at:"
50
+ puts " #{relative(dir, path)}\n\n"
51
+
52
+ puts "Choose an option:"
53
+ puts " 1) Overwrite entirely"
54
+ puts " 2) Show diff"
55
+ puts " 3) Abort"
56
+ print "\nEnter choice [1-3]: "
57
+
58
+ choice = STDIN.gets&.strip
59
+
60
+ case choice
61
+ when "1"
62
+ puts "\nOverwriting existing registry..."
63
+ :overwrite
64
+
65
+ when "2"
66
+ show_diff(dir: dir)
67
+ puts "\nAborting."
68
+ :abort
69
+
70
+ else
71
+ puts "\nAborting."
72
+ :abort
73
+ end
74
+ end
75
+
76
+ def relative(root, abs)
77
+ abs.sub(root + File::SEPARATOR, "")
78
+ end
79
+
80
+ def show_diff(dir:)
81
+ Vkit::CLI::Commands::RegistryDiffCommand.new.call(dir: dir)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -80,11 +80,20 @@ module Vkit
80
80
  exit 0
81
81
 
82
82
  when "ok"
83
- rows = result["rows"] || []
83
+ rows = result["rows"]
84
+ meta = result["meta"] || {}
85
+
86
+ unless rows.is_a?(Array)
87
+ raise "Invalid response: expected rows to be an array"
88
+ end
84
89
 
85
90
  puts "✅ OK — #{rows.size} rows"
86
- puts "ℹ️ Query Metadata:"
87
- puts JSON.pretty_generate(result["meta"] || {})
91
+
92
+ if meta.any?
93
+ puts "ℹ️ Query Metadata:"
94
+ puts JSON.pretty_generate(meta)
95
+ end
96
+
88
97
  puts
89
98
  puts "ℹ️ Data Rows:"
90
99
  Vkit::Core::TableFormatter.render(rows)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vkit
4
+ module Core
5
+ module Ansi
6
+ COLORS = {
7
+ red: 31,
8
+ green: 32,
9
+ yellow: 33,
10
+ blue: 34,
11
+ gray: 90
12
+ }.freeze
13
+
14
+ def self.color(text, color)
15
+ return text unless $stdout.tty?
16
+
17
+ code = COLORS[color]
18
+ "\e[#{code}m#{text}\e[0m"
19
+ end
20
+
21
+ def self.green(text) = color(text, :green)
22
+ def self.red(text) = color(text, :red)
23
+ def self.yellow(text) = color(text, :yellow)
24
+ def self.blue(text) = color(text, :blue)
25
+ def self.gray(text) = color(text, :gray)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vkit
4
+ module Core
5
+ class RegistryDiff
6
+ def self.compute(local:, remote:)
7
+ local ||= {}
8
+ remote ||= {}
9
+
10
+ local_datasets = normalize(local)
11
+ remote_datasets = normalize(remote)
12
+
13
+ out = { "datasets" => [] }
14
+
15
+ all_names = (local_datasets.keys | remote_datasets.keys).sort
16
+
17
+ all_names.each do |name|
18
+ local_fields = local_datasets[name] || []
19
+ remote_fields = remote_datasets[name] || []
20
+
21
+ changes = diff_fields(local_fields, remote_fields)
22
+
23
+ next if changes.values.all?(&:empty?)
24
+
25
+ out["datasets"] << {
26
+ "name" => name,
27
+ "changes" => changes
28
+ }
29
+ end
30
+
31
+ out
32
+ end
33
+
34
+ def self.normalize(registry)
35
+ return {} unless registry.is_a?(Hash)
36
+
37
+ # CASE 1: runtime export format
38
+ if registry.key?("datasets")
39
+ return Array(registry["datasets"]).each_with_object({}) do |ds, h|
40
+ h[ds["name"]] =
41
+ Array(ds["fields"]).map do |f|
42
+ {
43
+ "name" => f["name"].to_s,
44
+ "type" => f["type"].to_s,
45
+ "sensitivity" => f["sensitivity"].to_s,
46
+ "tags" => Array(f["tags"]).map(&:to_s).sort
47
+ }
48
+ end.sort_by { |f| f["name"] }
49
+ end
50
+ end
51
+
52
+ # CASE 2: local YAML format
53
+ registry.each_with_object({}) do |(dataset_name, ds), h|
54
+ fields = ds["fields"] || {}
55
+
56
+ normalized_fields =
57
+ fields.map do |field_name, meta|
58
+ {
59
+ "name" => field_name.to_s,
60
+ "type" => meta["type"].to_s,
61
+ "sensitivity" => meta["sensitivity"].to_s,
62
+ "tags" => [meta["category"]].compact.map(&:to_s).sort
63
+ }
64
+ end.sort_by { |f| f["name"] }
65
+
66
+ h[dataset_name.to_s] = normalized_fields
67
+ end
68
+ end
69
+
70
+
71
+ def self.diff_fields(local, remote)
72
+ l = local.each_with_object({}) { |f, h| h[f["name"]] = f }
73
+ r = remote.each_with_object({}) { |f, h| h[f["name"]] = f }
74
+
75
+ {
76
+ "added_fields" =>
77
+ (r.keys - l.keys).map { |k| r[k] },
78
+
79
+ "removed_fields" =>
80
+ (l.keys - r.keys).map { |k| l[k] },
81
+
82
+ "changed_fields" =>
83
+ (l.keys & r.keys).filter_map do |k|
84
+ next if l[k] == r[k]
85
+ { "name" => k, "local" => l[k], "remote" => r[k] }
86
+ end
87
+ }
88
+ end
89
+
90
+ private_class_method :diff_fields, :normalize
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ansi"
4
+
5
+ module Vkit
6
+ module Core
7
+ class RegistryDiffPrinter
8
+ def self.print(diff)
9
+ datasets = diff["datasets"] || []
10
+
11
+ summary = summarize(datasets)
12
+
13
+ if summary[:total_changes].zero?
14
+ puts Ansi.green("✓ No differences between local registry and runtime.")
15
+ return
16
+ end
17
+
18
+ print_summary(summary)
19
+ puts
20
+
21
+ datasets.each do |ds|
22
+ changes = ds["changes"]
23
+ next if changes.values.all?(&:empty?)
24
+
25
+ puts Ansi.blue("Dataset: #{ds['name']}")
26
+ puts
27
+
28
+ print_added(changes["added_fields"])
29
+ print_removed(changes["removed_fields"])
30
+ print_changed(changes["changed_fields"])
31
+
32
+ puts "-" * 50
33
+ end
34
+ end
35
+
36
+ def self.summarize(datasets)
37
+ datasets.each_with_object(
38
+ datasets_changed: 0,
39
+ added: 0,
40
+ removed: 0,
41
+ changed: 0,
42
+ total_changes: 0
43
+ ) do |ds, acc|
44
+ changes = ds["changes"] || {}
45
+
46
+ a = Array(changes["added_fields"]).size
47
+ r = Array(changes["removed_fields"]).size
48
+ c = Array(changes["changed_fields"]).size
49
+
50
+ next if a + r + c == 0
51
+
52
+ acc[:datasets_changed] += 1
53
+ acc[:added] += a
54
+ acc[:removed] += r
55
+ acc[:changed] += c
56
+ acc[:total_changes] += (a + r + c)
57
+ end
58
+ end
59
+
60
+ def self.print_summary(summary)
61
+ line = [
62
+ "#{summary[:datasets_changed]} datasets changed",
63
+ "#{summary[:added]} added",
64
+ "#{summary[:removed]} removed",
65
+ "#{summary[:changed]} modified"
66
+ ].join(" | ")
67
+
68
+ puts Ansi.yellow(line)
69
+ end
70
+
71
+ def self.print_added(fields)
72
+ return if fields.empty?
73
+
74
+ puts Ansi.green(" + Added Fields:")
75
+ fields.each do |f|
76
+ puts Ansi.green(" + #{f['name']} (#{f['type']})")
77
+ end
78
+ puts
79
+ end
80
+
81
+ def self.print_removed(fields)
82
+ return if fields.empty?
83
+
84
+ puts Ansi.red(" - Removed Fields:")
85
+ fields.each do |f|
86
+ puts Ansi.red(" - #{f['name']} (#{f['type']})")
87
+ end
88
+ puts
89
+ end
90
+
91
+ def self.print_changed(fields)
92
+ return if fields.empty?
93
+
94
+ puts Ansi.yellow(" ~ Changed Fields:")
95
+ fields.each do |f|
96
+ puts Ansi.yellow(" ~ #{f['name']}")
97
+ puts Ansi.gray(" From: #{f['from']}")
98
+ puts Ansi.gray(" To: #{f['to']}")
99
+ end
100
+ puts
101
+ end
102
+
103
+ private_class_method :summarize,
104
+ :print_summary,
105
+ :print_added,
106
+ :print_removed,
107
+ :print_changed
108
+ end
109
+ end
110
+ end
@@ -104,7 +104,31 @@ module Vkit
104
104
 
105
105
  def self.extract_masking(p)
106
106
  return unless p.dig("action", "mask")
107
- p["masking"]
107
+
108
+ raw =
109
+ p["masking"] ||
110
+ p.dig("action", "masking") || {}
111
+
112
+ default_method =
113
+ normalize_mask_method(raw["default_method"]) if raw["default_method"]
114
+
115
+ rules =
116
+ case raw["rules"]
117
+ when Hash
118
+ raw["rules"].each_with_object({}) do |(field, method), acc|
119
+ acc[field.to_s] = normalize_mask_method(method)
120
+ end
121
+ else
122
+ {}
123
+ end
124
+
125
+ result = {}
126
+ result["default_method"] = default_method if default_method
127
+ result["rules"] = rules if rules.any?
128
+
129
+ return if result.empty?
130
+
131
+ result
108
132
  end
109
133
 
110
134
  def self.normalize_registry(raw)
@@ -139,6 +163,16 @@ module Vkit
139
163
  end
140
164
  end
141
165
 
166
+ def self.normalize_mask_method(method)
167
+ case method.to_s
168
+ when "redact" then "full"
169
+ when "hash" then "hash"
170
+ when "truncate" then "partial"
171
+ when "nullify" then "full"
172
+ else "full"
173
+ end
174
+ end
175
+
142
176
  # Canonicalization
143
177
  def self.canonical_json(obj)
144
178
  JSON.generate(sort_keys_deep(obj))
@@ -13,3 +13,6 @@ action:
13
13
  mask: true
14
14
  reason: "Agents should not receive raw sensitive fields by default."
15
15
  ttl: "4h"
16
+
17
+ masking:
18
+ default_method: redact
@@ -11,4 +11,7 @@ context: {}
11
11
  action:
12
12
  mask: true
13
13
  reason: "Payment tokens/card-related data must never be exposed in raw form."
14
- ttl: "24h"
14
+ ttl: 24h
15
+
16
+ masking:
17
+ default_method: redact
@@ -1,13 +1,16 @@
1
1
  id: mask_pii_by_default
2
2
  description: "Mask PII fields by default unless explicitly allowed."
3
3
 
4
+ priority: 80
5
+
4
6
  match:
5
7
  fields:
6
- sensitivity: pii
7
-
8
- priority: 80
8
+ contains: ["pii"]
9
9
 
10
10
  action:
11
11
  mask: true
12
12
  reason: "PII is masked by default for safety."
13
- ttl: "1h"
13
+ ttl: 1h
14
+
15
+ masking:
16
+ default_method: redact
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Vkit
2
4
  module Policy
3
5
  class PolicyValidator
@@ -28,7 +30,7 @@ module Vkit
28
30
  MSG
29
31
  end
30
32
 
31
- validate_action!(action, prefix)
33
+ validate_action!(action, policy, prefix)
32
34
  true
33
35
  end
34
36
 
@@ -41,7 +43,7 @@ module Vkit
41
43
  MSG
42
44
  end
43
45
 
44
- def self.validate_action!(action, prefix)
46
+ def self.validate_action!(action, policy, prefix)
45
47
  intents = ACTION_KEYS.select { |k| action[k] == true }
46
48
 
47
49
  if intents.empty?
@@ -81,7 +83,8 @@ module Vkit
81
83
  require_string!(action, "approver_role", prefix)
82
84
 
83
85
  when "mask"
84
- # masking config can be validated later
86
+ # masking is optional, but if present must be valid
87
+ validate_masking!(policy, prefix) if policy.key?("masking")
85
88
 
86
89
  when "allow"
87
90
  # nothing required
@@ -124,6 +127,73 @@ module Vkit
124
127
  MSG
125
128
  end
126
129
  end
130
+
131
+ # Masking validation (aligned with compiler + runtime)
132
+ def self.validate_masking!(policy, prefix)
133
+ masking = policy["masking"]
134
+ return unless masking
135
+
136
+ unless masking.is_a?(Hash)
137
+ raise ValidationError, "#{prefix}masking must be a mapping."
138
+ end
139
+
140
+ allowed_keys = %w[default_method rules]
141
+ unknown_keys = masking.keys - allowed_keys
142
+
143
+ if unknown_keys.any?
144
+ raise ValidationError, <<~MSG
145
+ #{prefix}Unknown keys in masking: #{unknown_keys.join(', ')}
146
+
147
+ Allowed keys:
148
+ - default_method
149
+ - rules
150
+ MSG
151
+ end
152
+
153
+ if masking["default_method"]
154
+ validate_mask_method!(
155
+ masking["default_method"],
156
+ prefix,
157
+ "masking.default_method"
158
+ )
159
+ end
160
+
161
+ if masking["rules"]
162
+ unless masking["rules"].is_a?(Hash)
163
+ raise ValidationError,
164
+ "#{prefix}masking.rules must be a mapping of field → method."
165
+ end
166
+
167
+ masking["rules"].each do |field, method|
168
+ validate_mask_method!(
169
+ method,
170
+ prefix,
171
+ "masking.rules.#{field}"
172
+ )
173
+ end
174
+ end
175
+ end
176
+
177
+ def self.validate_mask_method!(method, prefix, path)
178
+ allowed = %w[redact hash truncate nullify full partial]
179
+
180
+ unless allowed.include?(method.to_s)
181
+ raise ValidationError, <<~MSG
182
+ #{prefix}Invalid masking method at #{path}.
183
+
184
+ Allowed values:
185
+ - redact
186
+ - hash
187
+ - truncate
188
+ - nullify
189
+ - full
190
+ - partial
191
+
192
+ Got:
193
+ #{method.inspect}
194
+ MSG
195
+ end
196
+ end
127
197
  end
128
198
  end
129
199
  end
@@ -264,19 +264,15 @@
264
264
  "type": "object",
265
265
  "additionalProperties": false,
266
266
  "properties": {
267
- "fields": {
268
- "type": "array",
269
- "items": {
270
- "type": "object",
271
- "required": ["name", "method"],
272
- "additionalProperties": false,
273
- "properties": {
274
- "name": { "type": "string" },
275
- "method": {
276
- "type": "string",
277
- "enum": ["redact", "hash", "truncate", "nullify"]
278
- }
279
- }
267
+ "default_method": {
268
+ "type": "string",
269
+ "enum": ["full", "hash", "partial"]
270
+ },
271
+ "rules": {
272
+ "type": "object",
273
+ "additionalProperties": {
274
+ "type": "string",
275
+ "enum": ["full", "hash", "partial"]
280
276
  }
281
277
  }
282
278
  }
data/lib/vkit/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vkit
4
- VERSION = "1.0.6"
4
+ VERSION = "1.0.8"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vaultkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.6
4
+ version: 1.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nnamdi Ogundu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-16 00:00:00.000000000 Z
11
+ date: 2026-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -61,6 +61,8 @@ files:
61
61
  - lib/vkit/cli/commands/policy_pack_upgrade_command.rb
62
62
  - lib/vkit/cli/commands/policy_revoke_command.rb
63
63
  - lib/vkit/cli/commands/policy_validate_command.rb
64
+ - lib/vkit/cli/commands/registry_diff_command.rb
65
+ - lib/vkit/cli/commands/registry_export_command.rb
64
66
  - lib/vkit/cli/commands/request_command.rb
65
67
  - lib/vkit/cli/commands/requests_list_command.rb
66
68
  - lib/vkit/cli/commands/reset_command.rb
@@ -70,9 +72,12 @@ files:
70
72
  - lib/vkit/cli/policy_bundle_validator.rb
71
73
  - lib/vkit/cli/policy_pack/manager.rb
72
74
  - lib/vkit/cli/requests_cli.rb
75
+ - lib/vkit/core/ansi.rb
73
76
  - lib/vkit/core/auth_client.rb
74
77
  - lib/vkit/core/credential_resolver.rb
75
78
  - lib/vkit/core/credential_store.rb
79
+ - lib/vkit/core/registry_diff.rb
80
+ - lib/vkit/core/registry_diff_printer.rb
76
81
  - lib/vkit/core/table_formatter.rb
77
82
  - lib/vkit/policy/bundle_compiler.rb
78
83
  - lib/vkit/policy/packs/ai_safety/metadata.yaml