vaultkit 0.1.3 → 1.0.1

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: e7924ff85def4166e7acb533f23e56a94cbca1f3d049b7745a65f88423375807
4
- data.tar.gz: fdbb7973e3fc214695718efb96a38de8bf2c77548fd2ce10506b5bbae515b57c
3
+ metadata.gz: e4b12d7696d6e2ec91bdf7843bd8648a1c9f0fdce6c59a6c4cbc902b3e4739c8
4
+ data.tar.gz: 7ae893269eb4075c797294585af9f28ad5da121f892b11e7d2e12ea8f625e956
5
5
  SHA512:
6
- metadata.gz: b02054d1e6abd18c1986d8967148ac49ffb807bc031dbfddab30e7eb3055384e96bdb0911f2ba1b6d82e62f1fa6d99ad0a115d3380a71efe63103d4512e825ad
7
- data.tar.gz: 6481994bc927812c1d5327bcd37169b4d98c11662bf689df6ef6f3d35e876fc59fdf19f6d7188e5cfb3183191de169a911a57a2dae7bf48019cf245ee83f9e64
6
+ metadata.gz: 48fc209dc487ee6649012b5d59da82946b37339cf4d3e1875d34c98146e07c79b93147ea510b98674736b6baf81c422a317f699c287103d0fe354e9191aadfb0
7
+ data.tar.gz: 62970b31c4cc95ab323e8f2868e41c4214905e680b427a9fe2475621d3e705b0d690c1ad44c113abd5d0b4186398322485b617cca332376c66abdeaacf0d4f6d
@@ -31,6 +31,11 @@ module Vkit
31
31
  Commands::LogoutCommand.new.call
32
32
  end
33
33
 
34
+ desc "reset", "Clear all stored credentials and configuration"
35
+ def reset
36
+ Commands::ResetCommand.new.call
37
+ end
38
+
34
39
  # REQUEST
35
40
  desc "request", "Send an inline JSON AQL request (use --aql or pipe via STDIN)"
36
41
  option :aql, type: :string, desc: "AQL JSON payload (inline)"
@@ -71,6 +76,20 @@ module Vkit
71
76
  )
72
77
  end
73
78
 
79
+ desc "approvals:watch", "Watch pending approval requests"
80
+ option :interval, type: :numeric, default: 3, desc: "Polling interval in seconds"
81
+ option :format, type: :string, default: "table", enum: %w[table json], desc: "Output format (table for humans, json for automations)"
82
+ option :pretty, type: :boolean, default: false, desc: "Pretty-print JSON output"
83
+ option :since, type: :string, desc: "Only show approvals created after this time (ISO8601 or 10m, 2h)"
84
+ define_method("approvals:watch") do
85
+ Commands::ApprovalWatchCommand.new.call(
86
+ interval: options[:interval],
87
+ format: options[:format],
88
+ pretty: options[:pretty],
89
+ since: options[:since]
90
+ )
91
+ end
92
+
74
93
  # FETCH
75
94
  desc "fetch --grant ID", "Fetch data from Funl using a valid grant"
76
95
  option :grant, type: :string, required: true
@@ -168,6 +187,49 @@ module Vkit
168
187
  )
169
188
  end
170
189
  }
190
+
191
+ desc "agents SUBCOMMAND ...ARGS", "Manage agents and automation identities"
192
+ subcommand "agents", Class.new(Thor) {
193
+
194
+ # agents tokens SUBCOMMAND
195
+ desc "tokens SUBCOMMAND ...ARGS", "Manage agent tokens"
196
+ subcommand "tokens", Class.new(Thor) {
197
+
198
+ # agents tokens list
199
+ desc "list", "List tokens for an agent"
200
+ option :format, type: :string, default: "table", enum: %w[table json]
201
+ def list
202
+ Commands::AgentTokensListCommand.new.call(
203
+ agent: options[:agent],
204
+ format: options[:format]
205
+ )
206
+ end
207
+
208
+ # agents tokens create
209
+ desc "create", "Create a new agent token (automation identity)"
210
+ option :name, required: true, desc: "Human-readable name (e.g. billing-bot)"
211
+ option :expires_in, type: :string, desc: "Token lifetime (e.g. 1h, 24h, 30d)"
212
+ option :role, type: :string, default: "agent", desc: "Role assigned to this token"
213
+ def create
214
+ Commands::AgentTokensCreateCommand.new.call(
215
+ name: options[:name],
216
+ expires_in: options[:expires_in],
217
+ role: options[:role]
218
+ )
219
+ end
220
+
221
+ # agents tokens revoke
222
+ desc "revoke", "Revoke an agent token"
223
+ option :token, required: true, desc: "Token ID or prefix"
224
+ option :force, type: :boolean, default: false, desc: "Skip confirmation"
225
+ def revoke
226
+ Commands::AgentTokensRevokeCommand.new.call(
227
+ token: options[:token],
228
+ force: options[:force]
229
+ )
230
+ end
231
+ }
232
+ }
171
233
  end
172
234
  end
173
235
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class AgentTokensCreateCommand < BaseCommand
10
+ def call(options)
11
+ with_auth do
12
+ user = credential_store.user
13
+ org = user["organization_slug"]
14
+
15
+ response =
16
+ authenticated_client.post(
17
+ "/api/v1/orgs/#{org}/agent_tokens",
18
+ body: build_body(options)
19
+ )
20
+
21
+ token = response.fetch("token")
22
+ secret = response.fetch("secret")
23
+
24
+ puts "🔐 AGENT TOKEN ISSUED (shown once)"
25
+ puts
26
+
27
+ puts "🧠 Name: #{token["name"]}"
28
+ puts "🎭 Role: #{token["role"]}"
29
+ puts "🆔 Token ID: #{token["id"]}"
30
+ puts "🏷 Prefix: #{token["prefix"]}"
31
+ puts "⏳ Expires At: #{token["expires_at"] || "never"}"
32
+ puts
33
+
34
+ puts "🔑 TOKEN"
35
+ puts "────────────────────────────────────────"
36
+ puts secret
37
+ puts "────────────────────────────────────────"
38
+ puts
39
+
40
+ puts "⚠️ Store this token securely. It cannot be retrieved again."
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def build_body(options)
47
+ body = {
48
+ name: options.fetch(:name),
49
+ role: options[:role],
50
+ }
51
+
52
+ if options[:expires_in]
53
+ body[:expires_at] = parse_expires_in(options[:expires_in]).iso8601
54
+ end
55
+
56
+ body
57
+ end
58
+
59
+ def parse_expires_in(value)
60
+ number = value.to_i
61
+
62
+ case value
63
+ when /\A\d+h\z/
64
+ Time.now.utc + (number * 60 * 60)
65
+ when /\A\d+d\z/
66
+ Time.now.utc + (number * 24 * 60 * 60)
67
+ else
68
+ raise "Invalid expires_in format (use 1h, 24h, 30d)"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "../../core/table_formatter"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class AgentTokensListCommand < BaseCommand
10
+ def call(options)
11
+ format = options[:format] || "table"
12
+
13
+ with_auth do
14
+ user = credential_store.user
15
+ org = user["organization_slug"]
16
+
17
+ response =
18
+ authenticated_client.get(
19
+ "/api/v1/orgs/#{org}/agent_tokens",
20
+ params: build_query(options)
21
+ )
22
+
23
+ tokens = response["tokens"] || []
24
+
25
+ print_result(tokens, format)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def build_query(options)
32
+ {}.tap do |q|
33
+ q[:name] = options[:agent] if options[:agent]
34
+ end
35
+ end
36
+
37
+ def print_result(tokens, format)
38
+ if tokens.empty?
39
+ puts "No tokens found."
40
+ return
41
+ end
42
+
43
+ case format
44
+ when "json"
45
+ puts JSON.pretty_generate(tokens)
46
+
47
+ when "table"
48
+ Vkit::Core::TableFormatter.render(
49
+ tokens.map do |t|
50
+ {
51
+ "ID" => t["id"],
52
+ "Agent" => t["name"],
53
+ "Prefix" => t["prefix"],
54
+ "Expires At" => t["expires_at"] || "never",
55
+ "Revoked At" => t["revoked_at"] || "-",
56
+ "Created At" => t["created_at"]
57
+ }
58
+ end
59
+ )
60
+
61
+ else
62
+ raise "Unknown format: #{format}"
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Vkit
6
+ module CLI
7
+ module Commands
8
+ class AgentTokensRevokeCommand < BaseCommand
9
+ def call(options)
10
+ token_ref = options.fetch(:token)
11
+
12
+ with_auth do
13
+ user = credential_store.user
14
+ org = user["organization_slug"]
15
+
16
+ token = resolve_token!(org, token_ref)
17
+
18
+ unless options[:force]
19
+ confirm!(token)
20
+ end
21
+
22
+ authenticated_client.post(
23
+ "/api/v1/orgs/#{org}/agent_tokens/#{token["id"]}/revoke",
24
+ body: {}
25
+ )
26
+
27
+ puts "🛑 AGENT TOKEN REVOKED"
28
+ puts
29
+ puts "🧠 Name: #{token["name"]}"
30
+ puts "🏷 Prefix: #{token["prefix"]}"
31
+ puts "🆔 ID: #{token["id"]}"
32
+ puts
33
+ puts "✅ The token is no longer valid."
34
+ end
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_token!(org, token_ref)
40
+ response =
41
+ authenticated_client.get(
42
+ "/api/v1/orgs/#{org}/agent_tokens"
43
+ )
44
+
45
+ tokens = response["tokens"] || []
46
+
47
+ token =
48
+ tokens.find do |t|
49
+ t["id"] == token_ref || t["prefix"] == token_ref
50
+ end
51
+
52
+ raise "Agent token not found: #{token_ref}" unless token
53
+
54
+ token
55
+ end
56
+
57
+ def confirm!(token)
58
+ puts "⚠️ You are about to revoke this agent token:"
59
+ puts
60
+ puts "🧠 Name: #{token["name"]}"
61
+ puts "🏷 Prefix: #{token["prefix"]}"
62
+ puts "🆔 ID: #{token["id"]}"
63
+ puts
64
+ print "\nType 'revoke' to confirm: "
65
+
66
+ input = STDIN.gets&.strip
67
+ abort "Aborted." unless input == "revoke"
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Vkit
7
+ module CLI
8
+ module Commands
9
+ class ApprovalWatchCommand < BaseCommand
10
+ DEFAULT_INTERVAL = 3
11
+
12
+ def call(interval: DEFAULT_INTERVAL, format: "table", pretty: false, since: nil)
13
+ with_auth do
14
+ user = credential_store.user
15
+ org = user["organization_slug"]
16
+
17
+ since_time = parse_since(since)
18
+ seen = {}
19
+
20
+ if format == "table"
21
+ puts "⏳ Watching approval queue (Ctrl+C to stop)…"
22
+ puts "Since: #{since_time.iso8601}" if since_time
23
+ puts
24
+ end
25
+
26
+ loop do
27
+ approvals =
28
+ authenticated_client.get(
29
+ "/api/v1/orgs/#{org}/approvals",
30
+ params: build_query(since_time)
31
+ )
32
+
33
+ approvals.each do |approval|
34
+ next if seen[approval["id"]]
35
+
36
+ emit(approval, format, pretty)
37
+ seen[approval["id"]] = true
38
+ end
39
+
40
+ sleep interval
41
+ end
42
+ end
43
+ rescue Interrupt
44
+ puts "\n👋 Watch stopped." if format == "table"
45
+ end
46
+
47
+ private
48
+
49
+ def build_query(since_time)
50
+ {}.tap do |q|
51
+ q[:state] = "pending"
52
+ q[:since] = since_time.iso8601 if since_time
53
+ end
54
+ end
55
+
56
+ def parse_since(value)
57
+ return nil if value.nil?
58
+
59
+ now = Time.now.utc
60
+
61
+ case value
62
+ when /\A(\d+)m\z/
63
+ now - Regexp.last_match(1).to_i * 60
64
+ when /\A(\d+)h\z/
65
+ now - Regexp.last_match(1).to_i * 60 * 60
66
+ when /\A(\d+)d\z/
67
+ now - Regexp.last_match(1).to_i * 60 * 60 * 24
68
+ else
69
+ Time.iso8601(value)
70
+ end
71
+ rescue ArgumentError
72
+ raise "Invalid --since value (use ISO8601 or 10m, 2h, 1d)"
73
+ end
74
+
75
+ def emit(approval, format, pretty)
76
+ case format
77
+ when "json"
78
+ print_json(normalize(approval), pretty: pretty)
79
+ when "table"
80
+ render_human(approval)
81
+ else
82
+ raise "Unknown format: #{format}"
83
+ end
84
+ end
85
+
86
+ def print_json(obj, pretty:)
87
+ if pretty
88
+ puts JSON.pretty_generate(obj)
89
+ else
90
+ puts JSON.generate(obj)
91
+ end
92
+ end
93
+
94
+ def normalize(a)
95
+ {
96
+ type: "approval.pending",
97
+ id: a["id"],
98
+ dataset: a["dataset"],
99
+ fields: a["fields"],
100
+ requester: a["requester"],
101
+ approver_role: a["approver_role"],
102
+ created_at: a["created_at"]
103
+ }
104
+ end
105
+
106
+ def render_human(a)
107
+ puts "🔔 NEW APPROVAL"
108
+ puts "────────────────────────────"
109
+ puts "ID: #{a["id"]}"
110
+ puts "Dataset: #{a["dataset"]}"
111
+ puts "Fields: #{Array(a["fields"]).join(", ")}"
112
+ puts "Requester: #{a["requester"]}"
113
+ puts "Role Req: #{a["approver_role"] || "any"}"
114
+ puts "Created: #{format_time(a["created_at"])}"
115
+ puts
116
+ puts "▶ Approve: vkit approval approve #{a["id"]}"
117
+ puts "▶ Deny: vkit approval deny #{a["id"]} --reason \"…\""
118
+ puts
119
+ end
120
+
121
+ def format_time(value)
122
+ Time.parse(value).getlocal.strftime("%Y-%m-%d %H:%M:%S")
123
+ rescue
124
+ value
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -16,8 +16,8 @@ module Vkit
16
16
  body: {}
17
17
  )
18
18
 
19
- rows = response["rows"] || []
20
- meta = response["meta"] || {}
19
+ rows = response.dig("rows", "rows") || []
20
+ meta = response.dig("rows", "meta") || {}
21
21
 
22
22
  print_result(rows, meta, format)
23
23
  end
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require_relative "../../policy/bundle_compiler"
6
+ require_relative "../../policy/validation_error"
6
7
 
7
8
  module Vkit
8
9
  module CLI
@@ -58,6 +59,9 @@ module Vkit
58
59
  puts " Checksum: #{bundle.dig("bundle", "checksum")}"
59
60
  puts " Output: #{out}"
60
61
  end
62
+ rescue Vkit::Policy::ValidationError => e
63
+ puts e.message
64
+ exit 1
61
65
  end
62
66
 
63
67
  private
@@ -0,0 +1,20 @@
1
+ module Vkit
2
+ module CLI
3
+ module Commands
4
+ class ResetCommand < BaseCommand
5
+ def call
6
+ print "⚠️ This will clear all stored credentials. Continue? (y/N): "
7
+ response = $stdin.gets.chomp
8
+
9
+ unless response.downcase == 'y'
10
+ puts "Cancelled"
11
+ return
12
+ end
13
+
14
+ credential_store.clear!
15
+ puts "🧹 All credentials cleared"
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -2,6 +2,7 @@ require "json"
2
2
  require "yaml"
3
3
  require "digest"
4
4
  require "time"
5
+ require_relative "policy_validator"
5
6
 
6
7
  module Vkit
7
8
  module Policy
@@ -40,6 +41,9 @@ module Vkit
40
41
  files.map do |f|
41
42
  data = YAML.load_file(f)
42
43
  raise "Policy file #{f} must be a Hash" unless data.is_a?(Hash)
44
+
45
+ PolicyValidator.validate!(data, file: File.basename(f))
46
+
43
47
  data.merge("__file" => File.basename(f))
44
48
  end
45
49
  end
@@ -0,0 +1,129 @@
1
+ module Vkit
2
+ module Policy
3
+ class PolicyValidator
4
+ ACTION_KEYS = %w[deny require_approval mask allow].freeze
5
+
6
+ def self.validate!(policy, file: nil)
7
+ prefix = file ? "In #{file}:\n\n" : ""
8
+
9
+ require_string!(policy, "id", prefix)
10
+ require_string!(policy, "description", prefix)
11
+
12
+ action = policy["action"]
13
+ unless action.is_a?(Hash)
14
+ raise ValidationError, <<~MSG
15
+ #{prefix}Invalid action format.
16
+
17
+ Expected:
18
+
19
+ action:
20
+ deny: true
21
+
22
+ Got:
23
+
24
+ action: #{action.inspect}
25
+
26
+ VaultKit requires `action` to be a mapping so it can attach
27
+ metadata like reason, ttl, approvals, and masking rules.
28
+ MSG
29
+ end
30
+
31
+ validate_action!(action, prefix)
32
+ true
33
+ end
34
+
35
+ def self.require_string!(hash, key, prefix)
36
+ value = hash[key]
37
+ return if value.is_a?(String) && !value.strip.empty?
38
+
39
+ raise ValidationError, <<~MSG
40
+ #{prefix}#{key} is required and must be a non-empty string.
41
+ MSG
42
+ end
43
+
44
+ def self.validate_action!(action, prefix)
45
+ intents = ACTION_KEYS.select { |k| action[k] == true }
46
+
47
+ if intents.empty?
48
+ raise ValidationError, <<~MSG
49
+ #{prefix}Action must specify exactly one intent.
50
+
51
+ Choose one of:
52
+
53
+ - deny
54
+ - require_approval
55
+ - mask
56
+ - allow
57
+
58
+ Example:
59
+
60
+ action:
61
+ deny: true
62
+ MSG
63
+ end
64
+
65
+ if intents.size > 1
66
+ raise ValidationError, <<~MSG
67
+ #{prefix}Action specifies multiple intents: #{intents.join(', ')}.
68
+
69
+ Only one intent may be set to true.
70
+ MSG
71
+ end
72
+
73
+ intent = intents.first
74
+
75
+ case intent
76
+ when "deny"
77
+ require_reason!(action, prefix)
78
+
79
+ when "require_approval"
80
+ require_reason!(action, prefix)
81
+ require_string!(action, "approver_role", prefix)
82
+
83
+ when "mask"
84
+ # masking config can be validated later
85
+
86
+ when "allow"
87
+ # nothing required
88
+ end
89
+
90
+ validate_ttl!(action["ttl"], prefix) if action.key?("ttl")
91
+ end
92
+
93
+ def self.require_reason!(action, prefix)
94
+ reason = action["reason"]
95
+ return if reason.is_a?(String) && !reason.strip.empty?
96
+
97
+ raise ValidationError, <<~MSG
98
+ #{prefix}action.reason is required and must be a non-empty string
99
+ when using deny or require_approval.
100
+ MSG
101
+ end
102
+
103
+ def self.validate_ttl!(ttl, prefix)
104
+ case ttl
105
+ when Integer
106
+ return
107
+ when String
108
+ return if ttl.match?(/\A\d+[smhd]\z/)
109
+
110
+ raise ValidationError, <<~MSG
111
+ #{prefix}Invalid ttl format.
112
+
113
+ Expected examples:
114
+ - "30m"
115
+ - "1h"
116
+ - "2d"
117
+
118
+ Got:
119
+ ttl: #{ttl.inspect}
120
+ MSG
121
+ else
122
+ raise ValidationError, <<~MSG
123
+ #{prefix}ttl must be a string duration (e.g. "1h") or an integer (seconds).
124
+ MSG
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,5 @@
1
+ module Vkit
2
+ module Policy
3
+ class ValidationError < StandardError; end
4
+ end
5
+ end
data/lib/vkit/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vkit
4
- VERSION = "0.1.3"
4
+ VERSION = "1.0.1"
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: 0.1.3
4
+ version: 1.0.1
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-01-10 00:00:00.000000000 Z
11
+ date: 2026-01-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -40,7 +40,11 @@ files:
40
40
  - lib/vkit/cli/api/client.rb
41
41
  - lib/vkit/cli/base_cli.rb
42
42
  - lib/vkit/cli/commands.rb
43
+ - lib/vkit/cli/commands/agent_tokens_create_command.rb
44
+ - lib/vkit/cli/commands/agent_tokens_list_command.rb
45
+ - lib/vkit/cli/commands/agent_tokens_revoke_command.rb
43
46
  - lib/vkit/cli/commands/approval_command.rb
47
+ - lib/vkit/cli/commands/approval_watch_command.rb
44
48
  - lib/vkit/cli/commands/base_command.rb
45
49
  - lib/vkit/cli/commands/datasource_command.rb
46
50
  - lib/vkit/cli/commands/fetch_command.rb
@@ -51,6 +55,7 @@ files:
51
55
  - lib/vkit/cli/commands/policy_validate_command.rb
52
56
  - lib/vkit/cli/commands/request_command.rb
53
57
  - lib/vkit/cli/commands/requests_list_command.rb
58
+ - lib/vkit/cli/commands/reset_command.rb
54
59
  - lib/vkit/cli/commands/scan_command.rb
55
60
  - lib/vkit/cli/commands/whoami_command.rb
56
61
  - lib/vkit/cli/errors.rb
@@ -61,8 +66,10 @@ files:
61
66
  - lib/vkit/core/credential_store.rb
62
67
  - lib/vkit/core/table_formatter.rb
63
68
  - lib/vkit/policy/bundle_compiler.rb
69
+ - lib/vkit/policy/policy_validator.rb
64
70
  - lib/vkit/policy/schema/policy_bundle.schema.json
65
71
  - lib/vkit/policy/validate_bundle.rb
72
+ - lib/vkit/policy/validation_error.rb
66
73
  - lib/vkit/utils/banner.rb
67
74
  - lib/vkit/utils/config_loader.rb
68
75
  - lib/vkit/utils/logger.rb