Class: Mysigner::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/mysigner/config.rb

Constant Summary collapse

CONFIG_DIR =
File.expand_path('~/.mysigner').freeze
CONFIG_FILE =
File.join(CONFIG_DIR, 'config.yml').freeze
KEY_FILE =
File.join(CONFIG_DIR, '.encryption_key').freeze
KEYCHAIN_SERVICE =
'com.mysigner.cli'
KEYCHAIN_ACCOUNT =
'config_encryption_key'
ENV_API_TOKEN =

Environment variable names for CI/CD support

'MYSIGNER_API_TOKEN'
ENV_API_URL =
'MYSIGNER_API_URL'
ENV_EMAIL =
'MYSIGNER_EMAIL'
ENV_ORG_ID =
'MYSIGNER_ORG_ID'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfig

Returns a new instance of Config.



30
31
32
33
34
35
36
37
38
# File 'lib/mysigner/config.rb', line 30

def initialize
  @api_url = nil
  @user_email = nil
  @current_organization_id = nil
  @organizations = {}
  @encryption_enabled = true # Enable by default for security
  @from_env = false
  load if exists?
end

Instance Attribute Details

#api_urlObject

Returns the value of attribute api_url.



27
28
29
# File 'lib/mysigner/config.rb', line 27

def api_url
  @api_url
end

#current_organization_idObject

Returns the value of attribute current_organization_id.



27
28
29
# File 'lib/mysigner/config.rb', line 27

def current_organization_id
  @current_organization_id
end

#encryption_enabledObject

Returns the value of attribute encryption_enabled.



27
28
29
# File 'lib/mysigner/config.rb', line 27

def encryption_enabled
  @encryption_enabled
end

#organizationsObject (readonly)

Returns the value of attribute organizations.



28
29
30
# File 'lib/mysigner/config.rb', line 28

def organizations
  @organizations
end

#user_emailObject

Returns the value of attribute user_email.



27
28
29
# File 'lib/mysigner/config.rb', line 27

def user_email
  @user_email
end

Class Method Details

.env_configured?Boolean

Check if all required env vars are set for CI/CD mode

Returns:

  • (Boolean)


41
42
43
44
# File 'lib/mysigner/config.rb', line 41

def self.env_configured?
  ENV.fetch(ENV_API_TOKEN, nil) && !ENV[ENV_API_TOKEN].empty? &&
    ENV.fetch(ENV_ORG_ID, nil) && !ENV[ENV_ORG_ID].empty?
end

.from_envObject

Create a Config from environment variables (for CI/CD)



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/mysigner/config.rb', line 47

def self.from_env
  config = allocate
  config.instance_variable_set(:@encryption_enabled, false)
  config.instance_variable_set(:@from_env, true)

  org_id = ENV.fetch(ENV_ORG_ID, nil)
  token = ENV.fetch(ENV_API_TOKEN, nil)
  config.instance_variable_set(:@api_url, ENV[ENV_API_URL] || 'https://mysigner.dev')
  config.instance_variable_set(:@user_email, ENV.fetch(ENV_EMAIL, nil))
  config.instance_variable_set(:@current_organization_id, org_id.to_i)
  config.instance_variable_set(:@organizations, {
                                 org_id.to_s => { 'name' => 'CI', 'token' => token }
                               })

  config
end

Instance Method Details

#api_token(org_id = nil) ⇒ Object

Get API token for current organization (or specific org)



70
71
72
73
74
75
76
77
78
79
80
# File 'lib/mysigner/config.rb', line 70

def api_token(org_id = nil)
  org_id ||= @current_organization_id
  return nil if org_id.nil?

  org_data = @organizations[org_id.to_s]
  token = org_data&.dig('token')
  return nil if token.nil?

  # Decrypt if encrypted
  encrypted?(token) ? decrypt_token(token) : token
end

#clearObject

Clear all configuration



158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# File 'lib/mysigner/config.rb', line 158

def clear
  @api_url = nil
  @user_email = nil
  @current_organization_id = nil
  @organizations = {}

  File.delete(CONFIG_FILE) if exists?

  # On non-macOS the encryption key lives in a file fallback. Wipe it on
  # logout so a fresh login can mint a new key — otherwise the old key
  # would silently encrypt a new token that nobody else can decrypt.
  FileUtils.rm_f(KEY_FILE)

  # Phase 0: logout also purges the keystore cache so a shared machine
  # doesn't leave prior-user keystore blobs on disk.
  keystores_dir = File.expand_path('~/.mysigner/keystores')
  FileUtils.rm_rf(keystores_dir)

  true
rescue StandardError => e
  raise ConfigError, "Failed to clear config: #{e.message}"
end

#disable_encryption!Object

Disable encryption and decrypt all tokens



244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/mysigner/config.rb', line 244

def disable_encryption!
  return true unless @encryption_enabled

  # Decrypt all tokens first
  @organizations.each_value do |org_data|
    token = org_data['token']
    next if token.nil? || !encrypted?(token)

    org_data['token'] = decrypt_token(token)
  end

  @encryption_enabled = false
  save
  true
end

#displayObject

Display config (with masked tokens)



203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'lib/mysigner/config.rb', line 203

def display
  current_org_name = org_name(@current_organization_id) || '(not set)'
  current_token = api_token(@current_organization_id)

  display_data = {
    api_url: @api_url || '(not set)',
    user_email: @user_email || '(not set)',
    current_organization: "#{current_org_name} (ID: #{@current_organization_id || 'not set'})",
    current_token: current_token ? mask_token(current_token) : '(not set)'
  }

  # Show all organizations
  if @organizations.any?
    display_data[:all_organizations] = @organizations.map do |org_id, org_data|
      token_status = org_data['token'] ? '' : ''
      "#{org_data['name']} (ID: #{org_id}) #{token_status}"
    end.join(', ')
  end

  display_data
end

#enable_encryption!Object

Enable encryption and re-encrypt all tokens



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
# File 'lib/mysigner/config.rb', line 226

def enable_encryption!
  return true if @encryption_enabled

  @encryption_enabled = true

  # Re-encrypt all existing tokens
  @organizations.each_value do |org_data|
    token = org_data['token']
    next if token.nil? || encrypted?(token)

    org_data['token'] = encrypt_token(token)
  end

  save
  true
end

#encrypted_config?Boolean

Check if config uses encryption

Returns:

  • (Boolean)


261
262
263
# File 'lib/mysigner/config.rb', line 261

def encrypted_config?
  @organizations.values.any? { |org_data| encrypted?(org_data['token']) }
end

#exists?Boolean

Check if config file exists

Returns:

  • (Boolean)


182
183
184
# File 'lib/mysigner/config.rb', line 182

def exists?
  File.exist?(CONFIG_FILE)
end

#from_env?Boolean

Whether this config was loaded from environment variables

Returns:

  • (Boolean)


65
66
67
# File 'lib/mysigner/config.rb', line 65

def from_env?
  @from_env
end

#has_token_for_org?(org_id) ⇒ Boolean

Check if we have a token for a specific organization

Returns:

  • (Boolean)


92
93
94
95
# File 'lib/mysigner/config.rb', line 92

def has_token_for_org?(org_id)
  token = api_token(org_id)
  !token.nil? && !token.empty?
end

#loadObject

Load configuration from file



121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/mysigner/config.rb', line 121

def load
  return false unless exists?

  data = YAML.load_file(CONFIG_FILE)

  @api_url = data['api_url']
  @user_email = data['user_email']
  @current_organization_id = data['current_organization_id']
  @organizations = data['organizations'] || {}

  # Auto-detect encryption from config
  @encryption_enabled = encrypted_config?

  true
rescue StandardError => e
  raise ConfigError, "Failed to load config: #{e.message}"
end

#org_name(org_id = nil) ⇒ Object

Get organization name



98
99
100
101
102
103
104
# File 'lib/mysigner/config.rb', line 98

def org_name(org_id = nil)
  org_id ||= @current_organization_id
  return nil if org_id.nil?

  org_data = @organizations[org_id.to_s]
  org_data&.dig('name')
end

#organization_idObject



111
112
113
# File 'lib/mysigner/config.rb', line 111

def organization_id
  @current_organization_id
end

#organization_idsObject

Get all organization IDs



107
108
109
# File 'lib/mysigner/config.rb', line 107

def organization_ids
  @organizations.keys.map(&:to_i)
end

#remove_token_for_org(org_id) ⇒ Object

Remove token for specific organization



116
117
118
# File 'lib/mysigner/config.rb', line 116

def remove_token_for_org(org_id)
  @organizations.delete(org_id.to_s)
end

#saveObject

Save configuration to file



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/mysigner/config.rb', line 140

def save
  ensure_config_dir_exists

  data = {
    'api_url' => @api_url,
    'user_email' => @user_email,
    'current_organization_id' => @current_organization_id,
    'organizations' => @organizations
  }

  File.write(CONFIG_FILE, data.to_yaml)
  File.chmod(0o600, CONFIG_FILE) # Make file readable only by owner
  true
rescue StandardError => e
  raise ConfigError, "Failed to save config: #{e.message}"
end

#save_token_for_org(org_id, org_name, token) ⇒ Object

Save token for a specific organization



83
84
85
86
87
88
89
# File 'lib/mysigner/config.rb', line 83

def save_token_for_org(org_id, org_name, token)
  encrypted_token = @encryption_enabled ? encrypt_token(token) : token
  @organizations[org_id.to_s] = {
    'name' => org_name,
    'token' => encrypted_token
  }
end

#to_hObject

Get config as hash



194
195
196
197
198
199
200
# File 'lib/mysigner/config.rb', line 194

def to_h
  {
    api_url: @api_url,
    user_email: @user_email,
    current_organization_id: @current_organization_id
  }
end

#valid?Boolean

Check if configuration is complete (has required fields)

Returns:

  • (Boolean)


187
188
189
190
191
# File 'lib/mysigner/config.rb', line 187

def valid?
  !@api_url.nil? && !@api_url.empty? &&
    !@current_organization_id.nil? &&
    has_token_for_org?(@current_organization_id)
end