keeper_secrets_manager 17.0.3

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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +49 -0
  5. data/Gemfile +13 -0
  6. data/LICENSE +21 -0
  7. data/README.md +305 -0
  8. data/Rakefile +30 -0
  9. data/examples/basic_usage.rb +139 -0
  10. data/examples/config_string_example.rb +99 -0
  11. data/examples/debug_secrets.rb +84 -0
  12. data/examples/demo_list_secrets.rb +182 -0
  13. data/examples/download_files.rb +100 -0
  14. data/examples/flexible_records_example.rb +94 -0
  15. data/examples/folder_hierarchy_demo.rb +109 -0
  16. data/examples/full_demo.rb +176 -0
  17. data/examples/my_test_standalone.rb +176 -0
  18. data/examples/simple_test.rb +162 -0
  19. data/examples/storage_examples.rb +126 -0
  20. data/lib/keeper_secrets_manager/config_keys.rb +27 -0
  21. data/lib/keeper_secrets_manager/core.rb +1231 -0
  22. data/lib/keeper_secrets_manager/crypto.rb +348 -0
  23. data/lib/keeper_secrets_manager/dto/payload.rb +152 -0
  24. data/lib/keeper_secrets_manager/dto.rb +221 -0
  25. data/lib/keeper_secrets_manager/errors.rb +79 -0
  26. data/lib/keeper_secrets_manager/field_types.rb +152 -0
  27. data/lib/keeper_secrets_manager/folder_manager.rb +114 -0
  28. data/lib/keeper_secrets_manager/keeper_globals.rb +59 -0
  29. data/lib/keeper_secrets_manager/notation.rb +354 -0
  30. data/lib/keeper_secrets_manager/notation_enhancements.rb +67 -0
  31. data/lib/keeper_secrets_manager/storage.rb +254 -0
  32. data/lib/keeper_secrets_manager/totp.rb +140 -0
  33. data/lib/keeper_secrets_manager/utils.rb +196 -0
  34. data/lib/keeper_secrets_manager/version.rb +3 -0
  35. data/lib/keeper_secrets_manager.rb +38 -0
  36. metadata +82 -0
@@ -0,0 +1,221 @@
1
+ require 'json'
2
+ require 'ostruct'
3
+ require_relative 'dto/payload'
4
+
5
+ module KeeperSecretsManager
6
+ module Dto
7
+ # Base class for dynamic record handling
8
+ class KeeperRecord
9
+ attr_accessor :uid, :title, :type, :fields, :custom, :notes, :folder_uid, :data, :revision, :files
10
+
11
+ def initialize(attrs = {})
12
+ if attrs.is_a?(Hash)
13
+ # Support both raw API response and user-friendly creation
14
+ @uid = attrs['recordUid'] || attrs['uid'] || attrs[:uid]
15
+ @folder_uid = attrs['folderUid'] || attrs['folder_uid'] || attrs[:folder_uid]
16
+ @revision = attrs['revision'] || attrs[:revision] || 0
17
+
18
+ # Handle encrypted data or direct attributes
19
+ if attrs['data']
20
+ data = attrs['data'].is_a?(String) ? JSON.parse(attrs['data']) : attrs['data']
21
+ @title = data['title'] || ''
22
+ @type = data['type'] || 'login'
23
+ @fields = data['fields'] || []
24
+ @custom = data['custom'] || []
25
+ @notes = data['notes'] || ''
26
+ else
27
+ @title = attrs['title'] || attrs[:title] || ''
28
+ @type = attrs['type'] || attrs[:type] || 'login'
29
+ @fields = attrs['fields'] || attrs[:fields] || []
30
+ @custom = attrs['custom'] || attrs[:custom] || []
31
+ @notes = attrs['notes'] || attrs[:notes] || ''
32
+ end
33
+
34
+ @files = attrs['files'] || attrs[:files] || []
35
+ @data = attrs
36
+ end
37
+
38
+ # Ensure fields are always arrays of hashes
39
+ normalize_fields!
40
+ end
41
+
42
+ # Convert to hash for API submission
43
+ def to_h
44
+ {
45
+ 'uid' => uid,
46
+ 'title' => title,
47
+ 'type' => type,
48
+ 'fields' => fields,
49
+ 'custom' => custom,
50
+ 'notes' => notes,
51
+ 'folder_uid' => folder_uid
52
+ }.compact
53
+ end
54
+
55
+ # Find field by type or label
56
+ def get_field(type_or_label, custom_field = false)
57
+ field_array = custom_field ? custom : fields
58
+ field_array.find { |f| f['type'] == type_or_label || f['label'] == type_or_label }
59
+ end
60
+
61
+ # Get field value (always returns array)
62
+ def get_field_value(type_or_label, custom_field = false)
63
+ field = get_field(type_or_label, custom_field)
64
+ field ? field['value'] || [] : []
65
+ end
66
+
67
+ # Get single field value (first element)
68
+ def get_field_value_single(type_or_label, custom_field = false)
69
+ values = get_field_value(type_or_label, custom_field)
70
+ values.first
71
+ end
72
+
73
+ # Add or update field
74
+ def set_field(type, value, label = nil, custom_field = false)
75
+ field_array = custom_field ? @custom : @fields
76
+
77
+ # Ensure value is an array
78
+ value = [value] unless value.is_a?(Array)
79
+
80
+ # Find existing field
81
+ existing = field_array.find { |f| f['type'] == type || (label && f['label'] == label) }
82
+
83
+ if existing
84
+ existing['value'] = value
85
+ existing['label'] = label if label
86
+ else
87
+ new_field = { 'type' => type, 'value' => value }
88
+ new_field['label'] = label if label
89
+ field_array << new_field
90
+ end
91
+ end
92
+
93
+ # Dynamic field access methods
94
+ def method_missing(method, *args, &block)
95
+ method_name = method.to_s
96
+
97
+ # Handle setters
98
+ if method_name.end_with?('=')
99
+ field_name = method_name.chomp('=')
100
+ set_field(field_name, args.first)
101
+ # Handle getters
102
+ elsif common_field_types.include?(method_name)
103
+ get_field_value_single(method_name)
104
+ else
105
+ super
106
+ end
107
+ end
108
+
109
+ def respond_to_missing?(method, include_private = false)
110
+ method_name = method.to_s.chomp('=')
111
+ common_field_types.include?(method_name) || super
112
+ end
113
+
114
+ private
115
+
116
+ def normalize_fields!
117
+ @fields = normalize_field_array(@fields)
118
+ @custom = normalize_field_array(@custom)
119
+ end
120
+
121
+ def normalize_field_array(fields)
122
+ return [] unless fields.is_a?(Array)
123
+
124
+ fields.map do |field|
125
+ next field if field.is_a?(Hash)
126
+
127
+ # Convert to hash if needed
128
+ field.to_h
129
+ end
130
+ end
131
+
132
+ def common_field_types
133
+ %w[login password url fileRef oneTimeCode name phone email address
134
+ paymentCard bankAccount birthDate secureNote sshKey host
135
+ databaseType script passkey]
136
+ end
137
+ end
138
+
139
+ # Folder representation
140
+ class KeeperFolder
141
+ attr_accessor :uid, :name, :parent_uid, :folder_type, :folder_key, :records
142
+
143
+ def initialize(attrs = {})
144
+ @uid = attrs['folderUid'] || attrs['uid'] || attrs[:uid]
145
+ @name = attrs['name'] || attrs[:name]
146
+ @parent_uid = attrs['parentUid'] || attrs['parent_uid'] || attrs[:parent_uid] || attrs['parent']
147
+ @folder_type = attrs['folderType'] || attrs['folder_type'] || attrs[:folder_type] || 'user_folder'
148
+ @folder_key = attrs['folderKey'] || attrs['folder_key'] || attrs[:folder_key]
149
+ @records = attrs['records'] || attrs[:records] || []
150
+ end
151
+
152
+ def to_h
153
+ {
154
+ 'folderUid' => uid,
155
+ 'name' => name,
156
+ 'parentUid' => parent_uid,
157
+ 'folderType' => folder_type
158
+ }.compact
159
+ end
160
+ end
161
+
162
+ # File attachment representation
163
+ class KeeperFile
164
+ attr_accessor :uid, :name, :title, :mime_type, :size, :data, :url
165
+
166
+ def initialize(attrs = {})
167
+ @uid = attrs['fileUid'] || attrs['uid'] || attrs[:uid]
168
+ @name = attrs['name'] || attrs[:name]
169
+ @title = attrs['title'] || attrs[:title] || @name
170
+ @mime_type = attrs['mimeType'] || attrs['mime_type'] || attrs[:mime_type]
171
+ @size = attrs['size'] || attrs[:size]
172
+ @data = attrs['data'] || attrs[:data]
173
+ @url = attrs['url'] || attrs[:url]
174
+ end
175
+
176
+ def to_h
177
+ {
178
+ 'fileUid' => uid,
179
+ 'name' => name,
180
+ 'title' => title,
181
+ 'mimeType' => mime_type,
182
+ 'size' => size
183
+ }.compact
184
+ end
185
+ end
186
+
187
+ # Response wrapper
188
+ class SecretsManagerResponse
189
+ attr_accessor :records, :folders, :app_data, :warnings, :errors, :just_bound
190
+
191
+ def initialize(attrs = {})
192
+ @records = attrs[:records] || []
193
+ @folders = attrs[:folders] || []
194
+ @app_data = attrs[:app_data] || {}
195
+ @warnings = attrs[:warnings] || []
196
+ @errors = attrs[:errors] || []
197
+ @just_bound = attrs[:just_bound] || false
198
+ end
199
+ end
200
+
201
+ # Query options
202
+ class QueryOptions
203
+ attr_accessor :records_filter, :folders_filter
204
+
205
+ def initialize(records: nil, folders: nil)
206
+ @records_filter = records
207
+ @folders_filter = folders
208
+ end
209
+ end
210
+
211
+ # Create options
212
+ class CreateOptions
213
+ attr_accessor :folder_uid, :subfolder_uid
214
+
215
+ def initialize(folder_uid: nil, subfolder_uid: nil)
216
+ @folder_uid = folder_uid
217
+ @subfolder_uid = subfolder_uid
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,79 @@
1
+ module KeeperSecretsManager
2
+ # Base error class for all KSM errors
3
+ class Error < StandardError; end
4
+
5
+ # Configuration errors
6
+ class ConfigurationError < Error; end
7
+
8
+ # Authentication/authorization errors
9
+ class AuthenticationError < Error; end
10
+ class AccessDeniedError < AuthenticationError; end
11
+
12
+ # API/network errors
13
+ class NetworkError < Error
14
+ attr_reader :status_code, :response_body
15
+
16
+ def initialize(message, status_code: nil, response_body: nil)
17
+ super(message)
18
+ @status_code = status_code
19
+ @response_body = response_body
20
+ end
21
+ end
22
+
23
+ # Crypto errors
24
+ class CryptoError < Error; end
25
+ class DecryptionError < CryptoError; end
26
+ class EncryptionError < CryptoError; end
27
+
28
+ # Notation errors
29
+ class NotationError < Error; end
30
+
31
+ # Record errors
32
+ class RecordError < Error; end
33
+ class RecordNotFoundError < RecordError; end
34
+ class RecordValidationError < RecordError; end
35
+
36
+ # Server errors
37
+ class ServerError < Error
38
+ attr_reader :result_code, :message
39
+
40
+ def initialize(result_code, message = nil)
41
+ @result_code = result_code
42
+ @message = message || "Server error: #{result_code}"
43
+ super(@message)
44
+ end
45
+ end
46
+
47
+ # Specific server error types
48
+ class InvalidClientVersionError < ServerError; end
49
+ class InvalidTokenError < ServerError; end
50
+ class BadRequestError < ServerError; end
51
+ class RecordUidNotFoundError < ServerError; end
52
+ class FolderUidNotFoundError < ServerError; end
53
+ class AccessViolationError < ServerError; end
54
+ class ThrottledError < ServerError; end
55
+
56
+ # Error factory
57
+ class ErrorFactory
58
+ def self.from_server_response(result_code, message = nil)
59
+ case result_code
60
+ when 'invalid_client_version'
61
+ InvalidClientVersionError.new(result_code, message)
62
+ when 'invalid_client', 'invalid_token'
63
+ InvalidTokenError.new(result_code, message)
64
+ when 'bad_request'
65
+ BadRequestError.new(result_code, message)
66
+ when 'record_uid_not_found'
67
+ RecordUidNotFoundError.new(result_code, message)
68
+ when 'folder_uid_not_found'
69
+ FolderUidNotFoundError.new(result_code, message)
70
+ when 'access_violation'
71
+ AccessViolationError.new(result_code, message)
72
+ when 'throttled'
73
+ ThrottledError.new(result_code, message)
74
+ else
75
+ ServerError.new(result_code, message)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,152 @@
1
+ module KeeperSecretsManager
2
+ module FieldTypes
3
+ # Base field helper
4
+ class Field
5
+ attr_accessor :type, :label, :value, :required, :privacy_screen
6
+
7
+ def initialize(type:, value:, label: nil, required: false, privacy_screen: false)
8
+ @type = type
9
+ @label = label
10
+ @required = required
11
+ @privacy_screen = privacy_screen
12
+
13
+ # Ensure value is always an array
14
+ @value = value.is_a?(Array) ? value : [value]
15
+ end
16
+
17
+ def to_h
18
+ h = { 'type' => type, 'value' => value }
19
+ h['label'] = label if label
20
+ h['required'] = required if required
21
+ h['privacyScreen'] = privacy_screen if privacy_screen
22
+ h
23
+ end
24
+ end
25
+
26
+ # Helper methods for creating common fields
27
+ module Helpers
28
+ def self.login(value, label: nil)
29
+ Field.new(type: 'login', value: value, label: label)
30
+ end
31
+
32
+ def self.password(value, label: nil)
33
+ Field.new(type: 'password', value: value, label: label)
34
+ end
35
+
36
+ def self.url(value, label: nil)
37
+ Field.new(type: 'url', value: value, label: label)
38
+ end
39
+
40
+ def self.file_ref(value, label: nil)
41
+ Field.new(type: 'fileRef', value: value, label: label)
42
+ end
43
+
44
+ def self.one_time_code(value, label: nil)
45
+ Field.new(type: 'oneTimeCode', value: value, label: label)
46
+ end
47
+
48
+ def self.name(first:, last:, middle: nil, label: nil)
49
+ value = { 'first' => first, 'last' => last }
50
+ value['middle'] = middle if middle
51
+ Field.new(type: 'name', value: value, label: label)
52
+ end
53
+
54
+ def self.phone(number:, region: 'US', type: nil, ext: nil, label: nil)
55
+ value = { 'region' => region, 'number' => number }
56
+ value['type'] = type if type
57
+ value['ext'] = ext if ext
58
+ Field.new(type: 'phone', value: value, label: label)
59
+ end
60
+
61
+ def self.email(email, label: nil)
62
+ Field.new(type: 'email', value: email, label: label)
63
+ end
64
+
65
+ def self.address(street1:, city:, state:, zip:, country: 'US', street2: nil, label: nil)
66
+ value = {
67
+ 'street1' => street1,
68
+ 'city' => city,
69
+ 'state' => state,
70
+ 'zip' => zip,
71
+ 'country' => country
72
+ }
73
+ value['street2'] = street2 if street2
74
+ Field.new(type: 'address', value: value, label: label)
75
+ end
76
+
77
+ def self.payment_card(number:, expiration_date:, security_code:, cardholder_name: nil, label: nil)
78
+ value = {
79
+ 'cardNumber' => number,
80
+ 'cardExpirationDate' => expiration_date,
81
+ 'cardSecurityCode' => security_code
82
+ }
83
+ value['cardholderName'] = cardholder_name if cardholder_name
84
+ Field.new(type: 'paymentCard', value: value, label: label)
85
+ end
86
+
87
+ def self.bank_account(account_type:, routing_number:, account_number:, label: nil)
88
+ value = {
89
+ 'accountType' => account_type,
90
+ 'routingNumber' => routing_number,
91
+ 'accountNumber' => account_number
92
+ }
93
+ Field.new(type: 'bankAccount', value: value, label: label)
94
+ end
95
+
96
+ def self.birth_date(date, label: nil)
97
+ # Date should be in unix timestamp (milliseconds)
98
+ timestamp = case date
99
+ when Date, Time, DateTime
100
+ (date.to_time.to_f * 1000).to_i
101
+ when Integer
102
+ date
103
+ when String
104
+ (Date.parse(date).to_time.to_f * 1000).to_i
105
+ else
106
+ raise ArgumentError, "Invalid date format"
107
+ end
108
+ Field.new(type: 'birthDate', value: timestamp, label: label)
109
+ end
110
+
111
+ def self.secure_note(note, label: nil)
112
+ Field.new(type: 'secureNote', value: note, label: label)
113
+ end
114
+
115
+ def self.ssh_key(private_key:, public_key: nil, label: nil)
116
+ value = { 'privateKey' => private_key }
117
+ value['publicKey'] = public_key if public_key
118
+ Field.new(type: 'sshKey', value: value, label: label)
119
+ end
120
+
121
+ def self.host(hostname:, port: nil, label: nil)
122
+ value = { 'hostName' => hostname }
123
+ value['port'] = port.to_s if port
124
+ Field.new(type: 'host', value: value, label: label)
125
+ end
126
+
127
+ def self.database_type(type, label: nil)
128
+ Field.new(type: 'databaseType', value: type, label: label)
129
+ end
130
+
131
+ def self.script(script, label: nil)
132
+ Field.new(type: 'script', value: script, label: label)
133
+ end
134
+
135
+ def self.passkey(private_key:, credential_id:, rp_id:, user_id:, username:, label: nil)
136
+ value = {
137
+ 'privateKey' => private_key,
138
+ 'credentialId' => credential_id,
139
+ 'relyingParty' => rp_id,
140
+ 'userId' => user_id,
141
+ 'username' => username
142
+ }
143
+ Field.new(type: 'passkey', value: value, label: label)
144
+ end
145
+
146
+ # Generic field for any type
147
+ def self.custom(type:, value:, label: nil, required: false)
148
+ Field.new(type: type, value: value, label: label, required: required)
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,114 @@
1
+ module KeeperSecretsManager
2
+ class FolderManager
3
+ def initialize(folders)
4
+ @folders = folders
5
+ end
6
+
7
+ # Build a hierarchical tree structure from flat folder list
8
+ def build_folder_tree
9
+ # Create a hash for quick lookup
10
+ folder_map = {}
11
+ @folders.each { |f| folder_map[f.uid] = f }
12
+
13
+ # Find root folders (no parent) and build tree
14
+ root_folders = []
15
+ @folders.each do |folder|
16
+ if folder.parent_uid.nil? || folder.parent_uid.empty?
17
+ root_folders << build_node(folder, folder_map)
18
+ end
19
+ end
20
+
21
+ root_folders
22
+ end
23
+
24
+ # Get folder path from root to given folder
25
+ def get_folder_path(folder_uid)
26
+ folder = @folders.find { |f| f.uid == folder_uid }
27
+ return nil unless folder
28
+
29
+ path = []
30
+ current = folder
31
+
32
+ # Walk up the tree
33
+ while current
34
+ path.unshift(current.name)
35
+ current = @folders.find { |f| f.uid == current.parent_uid }
36
+ end
37
+
38
+ path.join('/')
39
+ end
40
+
41
+ # Get all ancestors of a folder (parent, grandparent, etc.)
42
+ def get_ancestors(folder_uid)
43
+ ancestors = []
44
+ folder = @folders.find { |f| f.uid == folder_uid }
45
+ return ancestors unless folder
46
+
47
+ current_parent_uid = folder.parent_uid
48
+ while current_parent_uid && !current_parent_uid.empty?
49
+ parent = @folders.find { |f| f.uid == current_parent_uid }
50
+ break unless parent
51
+
52
+ ancestors << parent
53
+ current_parent_uid = parent.parent_uid
54
+ end
55
+
56
+ ancestors
57
+ end
58
+
59
+ # Get all descendants of a folder (children, grandchildren, etc.)
60
+ def get_descendants(folder_uid)
61
+ descendants = []
62
+ children = @folders.select { |f| f.parent_uid == folder_uid }
63
+
64
+ children.each do |child|
65
+ descendants << child
66
+ descendants.concat(get_descendants(child.uid))
67
+ end
68
+
69
+ descendants
70
+ end
71
+
72
+ # Find folder by name (optionally within a parent)
73
+ def find_folder_by_name(name, parent_uid: nil)
74
+ if parent_uid
75
+ @folders.find { |f| f.name == name && f.parent_uid == parent_uid }
76
+ else
77
+ @folders.find { |f| f.name == name }
78
+ end
79
+ end
80
+
81
+ # Print folder tree to console
82
+ def print_tree(folders = nil, indent = 0)
83
+ folders ||= build_folder_tree
84
+
85
+ folders.each do |node|
86
+ puts "#{' ' * indent}├── #{node[:folder].name} (#{node[:folder].uid})"
87
+ if node[:folder].records && !node[:folder].records.empty?
88
+ node[:folder].records.each do |record|
89
+ puts "#{' ' * (indent + 4)}└─ #{record.title} (#{record.uid})"
90
+ end
91
+ end
92
+ print_tree(node[:children], indent + 4) if node[:children]
93
+ end
94
+ end
95
+
96
+ private
97
+
98
+ def build_node(folder, folder_map)
99
+ node = {
100
+ folder: folder,
101
+ children: []
102
+ }
103
+
104
+ # Find children
105
+ @folders.each do |f|
106
+ if f.parent_uid == folder.uid
107
+ node[:children] << build_node(f, folder_map)
108
+ end
109
+ end
110
+
111
+ node
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,59 @@
1
+ require_relative 'version'
2
+
3
+ module KeeperSecretsManager
4
+ module KeeperGlobals
5
+ # Client version prefix
6
+ # **NOTE: 'mb' client version is NOT YET REGISTERED with Keeper servers!**
7
+ # **TODO: Register 'mb' (Ruby) client with Keeper before production use**
8
+ # **Currently using 'mr' which is already registered (likely Rust SDK)**
9
+ CLIENT_VERSION_PREFIX = 'mr'.freeze # Should be 'mb' for Ruby, but using 'mr' temporarily
10
+
11
+ # Get client version
12
+ def self.client_version
13
+ # Use standard version format matching other SDKs
14
+ # Java: mj17.0.0, Python: mp16.x.x, JavaScript: ms16.x.x, Go: mg16.x.x
15
+ # Ruby should be: mb17.0.0 (but not registered yet)
16
+ "#{CLIENT_VERSION_PREFIX}17.0.0"
17
+ end
18
+
19
+ # Keeper public keys by ID
20
+ KEEPER_PUBLIC_KEYS = {
21
+ '1' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
22
+ '2' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
23
+ '3' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
24
+ '4' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
25
+ '5' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
26
+ '6' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
27
+ '7' => 'BK9w6TZFxE6nFNbMfIpULCup2a8xc6w2tUTABjxny7yFmxW0dAEojwC6j6zb5nTlmb1dAx8nwo3qF7RPYGmloRM',
28
+ '8' => 'BKnhy0obglZJK-igwthNLdknoSXRrGB-mvFRzyb_L-DKKefWjYdFD2888qN1ROczz4n3keYSfKz9Koj90Z6w_tQ',
29
+ '9' => 'BAsPQdCpLIGXdWNLdAwx-3J5lNqUtKbaOMV56hUj8VzxE2USLHuHHuKDeno0ymJt-acxWV1xPlBfNUShhRTR77g',
30
+ '10' => 'BNYIh_Sv03nRZUUJveE8d2mxKLIDXv654UbshaItHrCJhd6cT7pdZ_XwbdyxAOCWMkBb9AZ4t1XRCsM8-wkEBRg',
31
+ '11' => 'BA6uNfeYSvqagwu4TOY6wFK4JyU5C200vJna0lH4PJ-SzGVXej8l9dElyQ58_ljfPs5Rq6zVVXpdDe8A7Y3WRhk',
32
+ '12' => 'BMjTIlXfohI8TDymsHxo0DqYysCy7yZGJ80WhgOBR4QUd6LBDA6-_318a-jCGW96zxXKMm8clDTKpE8w75KG-FY',
33
+ '13' => 'BJBDU1P1H21IwIdT2brKkPqbQR0Zl0TIHf7Bz_OO9jaNgIwydMkxt4GpBmkYoprZ_DHUGOrno2faB7pmTR7HhuI',
34
+ '14' => 'BJFF8j-dH7pDEw_U347w2CBM6xYM8Dk5fPPAktjib-opOqzvvbsER-WDHM4ONCSBf9O_obAHzCyygxmtpktDuiE',
35
+ '15' => 'BDKyWBvLbyZ-jMueORl3JwJnnEpCiZdN7yUvT0vOyjwpPBCDf6zfL4RWzvSkhAAFnwOni_1tQSl8dfXHbXqXsQ8',
36
+ '16' => 'BDXyZZnrl0tc2jdC5I61JjwkjK2kr7uet9tZjt8StTiJTAQQmnVOYBgbtP08PWDbecxnHghx3kJ8QXq1XE68y8c',
37
+ '17' => 'BFX68cb97m9_sweGdOVavFM3j5ot6gveg6xT4BtGahfGhKib-zdZyO9pwvv1cBda9ahkSzo1BQ4NVXp9qRyqVGU'
38
+ }.freeze
39
+
40
+ # Keeper servers by region
41
+ KEEPER_SERVERS = {
42
+ 'US' => 'keepersecurity.com',
43
+ 'EU' => 'keepersecurity.eu',
44
+ 'AU' => 'keepersecurity.com.au',
45
+ 'GOV' => 'govcloud.keepersecurity.us',
46
+ 'JP' => 'keepersecurity.jp',
47
+ 'CA' => 'keepersecurity.ca'
48
+ }.freeze
49
+
50
+ # Default server (US)
51
+ DEFAULT_SERVER = KEEPER_SERVERS['US'].freeze
52
+
53
+ # Default public key ID
54
+ DEFAULT_KEY_ID = '7'.freeze
55
+
56
+ # Logger name
57
+ LOGGER_NAME = 'keeper_secrets_manager'.freeze
58
+ end
59
+ end