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'
ENV_LOCAL_ONLY =
'MYSIGNER_LOCAL_ONLY'

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeConfig

Returns a new instance of Config.



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

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.



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

def api_url
  @api_url
end

#current_organization_idObject

Returns the value of attribute current_organization_id.



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

def current_organization_id
  @current_organization_id
end

#encryption_enabledObject

Returns the value of attribute encryption_enabled.



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

def encryption_enabled
  @encryption_enabled
end

#organizationsObject (readonly)

Returns the value of attribute organizations.



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

def organizations
  @organizations
end

#user_emailObject

Returns the value of attribute user_email.



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

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)


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

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)



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

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

.local_only?Boolean

Local-only mode: when true, credentials never leave the machine. Config-level check sees only ENV — Thor’s –local-only flag is layered on top in the CLI Helpers concern (which can read ‘options`).

Returns:

  • (Boolean)


73
74
75
# File 'lib/mysigner/config.rb', line 73

def self.local_only?
  truthy_env?(ENV_LOCAL_ONLY)
end

Instance Method Details

#api_token(org_id = nil) ⇒ Object

Get API token for current organization (or specific org)



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

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



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/mysigner/config.rb', line 178

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



264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
# File 'lib/mysigner/config.rb', line 264

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)



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

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



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

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)


281
282
283
# File 'lib/mysigner/config.rb', line 281

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

#exists?Boolean

Check if config file exists

Returns:

  • (Boolean)


202
203
204
# File 'lib/mysigner/config.rb', line 202

def exists?
  File.exist?(CONFIG_FILE)
end

#fetch_encryption_keyObject

Public accessor for the per-machine 32-byte AES-256-GCM key. Exposed so sibling stores (e.g. LocalCredentials) can encrypt secrets under the same key without duplicating the keychain/file fallback. The key itself is created on first read and is stable across calls.



289
290
291
# File 'lib/mysigner/config.rb', line 289

def fetch_encryption_key
  get_or_create_encryption_key
end

#from_env?Boolean

Whether this config was loaded from environment variables

Returns:

  • (Boolean)


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

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)


104
105
106
107
# File 'lib/mysigner/config.rb', line 104

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

#loadObject

Load configuration from file



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/mysigner/config.rb', line 133

def load
  return false unless exists?

  # mysigner-51 — safe_load_file rejects arbitrary Ruby object
  # instantiation in the YAML (`!ruby/object:Foo` etc.). The config
  # shape is just String/Integer/Hash (api_url, user_email,
  # current_organization_id, organizations: {id => {name, token}}),
  # all in safe_load's default allowed set, so no permitted_classes
  # extension is needed. Low risk (the file is 0600 and user-owned)
  # but cheap hardening against a future RCE if config write or read
  # ever moves outside the owner-only assumption.
  data = YAML.safe_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

#local_only?Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/mysigner/config.rb', line 77

def local_only?
  self.class.local_only?
end

#org_name(org_id = nil) ⇒ Object

Get organization name



110
111
112
113
114
115
116
# File 'lib/mysigner/config.rb', line 110

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



123
124
125
# File 'lib/mysigner/config.rb', line 123

def organization_id
  @current_organization_id
end

#organization_idsObject

Get all organization IDs



119
120
121
# File 'lib/mysigner/config.rb', line 119

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

#remove_token_for_org(org_id) ⇒ Object

Remove token for specific organization



128
129
130
# File 'lib/mysigner/config.rb', line 128

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

#saveObject

Save configuration to file



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

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



95
96
97
98
99
100
101
# File 'lib/mysigner/config.rb', line 95

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



214
215
216
217
218
219
220
# File 'lib/mysigner/config.rb', line 214

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)


207
208
209
210
211
# File 'lib/mysigner/config.rb', line 207

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