Class: Mysigner::Config
- Inherits:
-
Object
- Object
- Mysigner::Config
- Defined in:
- lib/mysigner/config.rb
Constant Summary collapse
- CONFIG_DIR =
File.('~/.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
-
#api_url ⇒ Object
Returns the value of attribute api_url.
-
#current_organization_id ⇒ Object
Returns the value of attribute current_organization_id.
-
#encryption_enabled ⇒ Object
Returns the value of attribute encryption_enabled.
-
#local_only ⇒ Object
Returns the value of attribute local_only.
-
#organizations ⇒ Object
readonly
Returns the value of attribute organizations.
-
#user_email ⇒ Object
Returns the value of attribute user_email.
Class Method Summary collapse
-
.env_configured? ⇒ Boolean
Check if all required env vars are set for CI/CD mode.
-
.from_env ⇒ Object
Create a Config from environment variables (for CI/CD).
-
.local_only? ⇒ Boolean
Local-only mode at the Config level: cascade ENV → file.
-
.local_only_from_env? ⇒ Boolean
Public predicate for the env-var source.
-
.local_only_from_file? ⇒ Boolean
Lightweight check that reads only ~/.mysigner/config.yml’s ‘local_only:` key without invoking #load (which decrypts tokens via the Keychain).
Instance Method Summary collapse
-
#api_token(org_id = nil) ⇒ Object
Get API token for current organization (or specific org).
-
#clear ⇒ Object
Clear all configuration.
-
#disable_encryption! ⇒ Object
Disable encryption and decrypt all tokens.
-
#display ⇒ Object
Display config (with masked tokens).
-
#enable_encryption! ⇒ Object
Enable encryption and re-encrypt all tokens.
-
#encrypted_config? ⇒ Boolean
Check if config uses encryption.
-
#exists? ⇒ Boolean
Check if config file exists.
-
#fetch_encryption_key ⇒ Object
Public accessor for the per-machine 32-byte AES-256-GCM key.
-
#from_env? ⇒ Boolean
Whether this config was loaded from environment variables.
-
#has_token_for_org?(org_id) ⇒ Boolean
Check if we have a token for a specific organization.
-
#initialize ⇒ Config
constructor
A new instance of Config.
-
#load ⇒ Object
Load configuration from file.
-
#local_only? ⇒ Boolean
Instance-level predicate.
-
#org_name(org_id = nil) ⇒ Object
Get organization name.
- #organization_id ⇒ Object
-
#organization_ids ⇒ Object
Get all organization IDs.
-
#remove_token_for_org(org_id) ⇒ Object
Remove token for specific organization.
-
#save ⇒ Object
Save configuration to file.
-
#save_token_for_org(org_id, org_name, token) ⇒ Object
Save token for a specific organization.
-
#to_h ⇒ Object
Get config as hash.
-
#valid? ⇒ Boolean
Check if configuration is complete (has required fields).
Constructor Details
#initialize ⇒ Config
Returns a new instance of Config.
31 32 33 34 35 36 37 38 39 40 |
# 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 @local_only = false @from_env = false load if exists? end |
Instance Attribute Details
#api_url ⇒ Object
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_id ⇒ Object
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_enabled ⇒ Object
Returns the value of attribute encryption_enabled.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def encryption_enabled @encryption_enabled end |
#local_only ⇒ Object
Returns the value of attribute local_only.
28 29 30 |
# File 'lib/mysigner/config.rb', line 28 def local_only @local_only end |
#organizations ⇒ Object (readonly)
Returns the value of attribute organizations.
29 30 31 |
# File 'lib/mysigner/config.rb', line 29 def organizations @organizations end |
#user_email ⇒ Object
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
43 44 45 46 |
# File 'lib/mysigner/config.rb', line 43 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_env ⇒ Object
Create a Config from environment variables (for CI/CD)
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
# File 'lib/mysigner/config.rb', line 49 def self.from_env config = allocate config.instance_variable_set(:@encryption_enabled, false) config.instance_variable_set(:@from_env, true) config.instance_variable_set(:@local_only, false) 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 at the Config level: cascade ENV → file. The CLI Helpers concern layers –local-only / –no-local-only on top.
74 75 76 |
# File 'lib/mysigner/config.rb', line 74 def self.local_only? local_only_from_env? || local_only_from_file? end |
.local_only_from_env? ⇒ Boolean
Public predicate for the env-var source. Mirrors local_only_from_file? so status’s “Source: …” attribution can distinguish env vs file using the same truthy parser the cascade uses (a literal env value of “0” / “false” reads as off, not as “env var enabled it”).
82 83 84 |
# File 'lib/mysigner/config.rb', line 82 def self.local_only_from_env? truthy_env?(ENV_LOCAL_ONLY) end |
.local_only_from_file? ⇒ Boolean
Lightweight check that reads only ~/.mysigner/config.yml’s ‘local_only:` key without invoking #load (which decrypts tokens via the Keychain). A user with no MySigner account still needs to flip this setting, so we never raise on a missing/corrupt or absent file — we just return false.
91 92 93 94 95 96 |
# File 'lib/mysigner/config.rb', line 91 def self.local_only_from_file? data = YAML.safe_load_file(CONFIG_FILE) data.is_a?(Hash) && data['local_only'] == true rescue Errno::ENOENT, Psych::SyntaxError false end |
Instance Method Details
#api_token(org_id = nil) ⇒ Object
Get API token for current organization (or specific org)
107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/mysigner/config.rb', line 107 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 |
#clear ⇒ Object
Clear all configuration
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 |
# File 'lib/mysigner/config.rb', line 206 def clear @api_url = nil @user_email = nil @current_organization_id = nil @organizations = {} @local_only = false 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.('~/.mysigner/keystores') FileUtils.rm_rf(keystores_dir) true rescue StandardError => e raise ConfigError, "Failed to clear config: #{e.}" end |
#disable_encryption! ⇒ Object
Disable encryption and decrypt all tokens
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 |
# File 'lib/mysigner/config.rb', line 293 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 |
#display ⇒ Object
Display config (with masked tokens)
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 |
# File 'lib/mysigner/config.rb', line 252 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
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'lib/mysigner/config.rb', line 275 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
310 311 312 |
# File 'lib/mysigner/config.rb', line 310 def encrypted_config? @organizations.values.any? { |org_data| encrypted?(org_data['token']) } end |
#exists? ⇒ Boolean
Check if config file exists
231 232 233 |
# File 'lib/mysigner/config.rb', line 231 def exists? File.exist?(CONFIG_FILE) end |
#fetch_encryption_key ⇒ Object
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.
318 319 320 |
# File 'lib/mysigner/config.rb', line 318 def fetch_encryption_key get_or_create_encryption_key end |
#from_env? ⇒ Boolean
Whether this config was loaded from environment variables
68 69 70 |
# File 'lib/mysigner/config.rb', line 68 def from_env? @from_env end |
#has_token_for_org?(org_id) ⇒ Boolean
Check if we have a token for a specific organization
129 130 131 132 |
# File 'lib/mysigner/config.rb', line 129 def has_token_for_org?(org_id) token = api_token(org_id) !token.nil? && !token.empty? end |
#load ⇒ Object
Load configuration from file
158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
# File 'lib/mysigner/config.rb', line 158 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/Boolean/Hash (api_url, user_email, # current_organization_id, local_only, 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'] || {} @local_only = data['local_only'] == true # Auto-detect encryption from config @encryption_enabled = encrypted_config? true rescue StandardError => e raise ConfigError, "Failed to load config: #{e.}" end |
#local_only? ⇒ Boolean
Instance-level predicate. Merges two surfaces:
- @local_only: set to true by Helpers#blank_local_only_config so a
sentinel config always answers true without touching ENV or disk.
- self.class.local_only?: the normal ENV → file cascade.
102 103 104 |
# File 'lib/mysigner/config.rb', line 102 def local_only? @local_only || self.class.local_only? end |
#org_name(org_id = nil) ⇒ Object
Get organization name
135 136 137 138 139 140 141 |
# File 'lib/mysigner/config.rb', line 135 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_id ⇒ Object
148 149 150 |
# File 'lib/mysigner/config.rb', line 148 def organization_id @current_organization_id end |
#organization_ids ⇒ Object
Get all organization IDs
144 145 146 |
# File 'lib/mysigner/config.rb', line 144 def organization_ids @organizations.keys.map(&:to_i) end |
#remove_token_for_org(org_id) ⇒ Object
Remove token for specific organization
153 154 155 |
# File 'lib/mysigner/config.rb', line 153 def remove_token_for_org(org_id) @organizations.delete(org_id.to_s) end |
#save ⇒ Object
Save configuration to file
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 |
# File 'lib/mysigner/config.rb', line 187 def save ensure_config_dir_exists data = { 'api_url' => @api_url, 'user_email' => @user_email, 'current_organization_id' => @current_organization_id, 'organizations' => @organizations, 'local_only' => @local_only } 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.}" end |
#save_token_for_org(org_id, org_name, token) ⇒ Object
Save token for a specific organization
120 121 122 123 124 125 126 |
# File 'lib/mysigner/config.rb', line 120 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_h ⇒ Object
Get config as hash
243 244 245 246 247 248 249 |
# File 'lib/mysigner/config.rb', line 243 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)
236 237 238 239 240 |
# File 'lib/mysigner/config.rb', line 236 def valid? !@api_url.nil? && !@api_url.empty? && !@current_organization_id.nil? && has_token_for_org?(@current_organization_id) end |