Skip to content

Commit 882468e

Browse files
markhallenclaude
andcommitted
fix: address PR review feedback
- Log warning when config file exists but CONFIG_PASSPHRASE is unset - Rescue ArgumentError in decrypt for corrupted/non-Base64 data - Add blob length validation before attempting decryption - Enforce unique slack_team_id in add_project and validate on update - Require non-empty passphrases in TUI prompts - Replace send() with explicit lambda dispatch table in menu loop - Add required: true to team_id field in edit flow - Show restart reminder when exiting TUI with configured projects Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 966d4ce commit 882468e

4 files changed

Lines changed: 42 additions & 12 deletions

File tree

app.rb

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,17 @@
5050
config_path = File.join(settings.root, '.config', 'projects.enc')
5151
config_passphrase = ENV.fetch('CONFIG_PASSPHRASE', nil)
5252

53-
if File.exist?(config_path) && config_passphrase
54-
project_config = Config::ProjectConfig.new(config_path: config_path)
55-
project_config.load!(config_passphrase)
56-
set :project_config, project_config
57-
settings.logger.info "Loaded #{project_config.projects.length} project(s) from encrypted config"
53+
if File.exist?(config_path)
54+
if config_passphrase
55+
project_config = Config::ProjectConfig.new(config_path: config_path)
56+
project_config.load!(config_passphrase)
57+
set :project_config, project_config
58+
settings.logger.info "Loaded #{project_config.projects.length} project(s) from encrypted config"
59+
else
60+
set :project_config, nil
61+
settings.logger.warn 'Encrypted project config found but CONFIG_PASSPHRASE is not set; ' \
62+
'falling back to environment variable credentials'
63+
end
5864
else
5965
set :project_config, nil
6066
end

lib/cli/tui.rb

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ def run
1818
@passphrase = prompt_passphrase
1919
@config.load!(@passphrase)
2020
main_menu_loop
21+
@prompt.warn('Restart the running app for config changes to take effect.') unless @config.empty?
2122
rescue Config::Encryption::DecryptionError
2223
@prompt.error('Wrong passphrase. Unable to decrypt config file.')
2324
exit 1
@@ -27,11 +28,11 @@ def run
2728

2829
def prompt_passphrase
2930
if File.exist?(@config_path)
30-
@prompt.mask('Enter config passphrase:')
31+
@prompt.mask('Enter config passphrase:', required: true)
3132
else
3233
@prompt.ok('No config file found. Creating a new one.')
33-
passphrase = @prompt.mask('Choose a passphrase for encrypting your config:')
34-
confirm = @prompt.mask('Confirm passphrase:')
34+
passphrase = @prompt.mask('Choose a passphrase for encrypting your config:', required: true)
35+
confirm = @prompt.mask('Confirm passphrase:', required: true)
3536
unless passphrase == confirm
3637
@prompt.error('Passphrases do not match.')
3738
exit 1
@@ -41,12 +42,19 @@ def prompt_passphrase
4142
end
4243

4344
def main_menu_loop
45+
handlers = {
46+
add_project: -> { add_project },
47+
edit_project: -> { edit_project },
48+
remove_project: -> { remove_project },
49+
list_projects: -> { list_projects },
50+
}
51+
4452
loop do
4553
choices = build_menu_choices
4654
action = @prompt.select('What would you like to do?', choices)
4755
break if action == :exit
4856

49-
send(action)
57+
handlers[action]&.call
5058
end
5159
end
5260

@@ -94,8 +102,8 @@ def edit_project
94102
project = @config.find_by_name(name)
95103
updates = {}
96104

97-
updates[:name] = @prompt.ask('Project name:', default: project[:name])
98-
updates[:slack_team_id] = @prompt.ask('Slack team ID:', default: project[:slack_team_id])
105+
updates[:name] = @prompt.ask('Project name:', default: project[:name], required: true)
106+
updates[:slack_team_id] = @prompt.ask('Slack team ID:', default: project[:slack_team_id], required: true)
99107
updates[:slack_bot_token] = prompt_optional_mask('Slack bot token:', project[:slack_bot_token])
100108
updates[:github_token] = prompt_optional_mask('GitHub token:', project[:github_token])
101109
updates[:default_github_org] = @prompt.ask('Default GitHub org:', default: project[:default_github_org])

lib/config/encryption.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ def self.encrypt(plaintext, passphrase)
3131
Base64.strict_encode64(blob)
3232
end
3333

34+
MIN_BLOB_LENGTH = SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH
35+
3436
def self.decrypt(encoded_blob, passphrase)
3537
blob = Base64.strict_decode64(encoded_blob)
38+
raise DecryptionError, 'Data too short — file may be corrupted' if blob.bytesize < MIN_BLOB_LENGTH
3639

3740
salt = blob[0, SALT_LENGTH]
3841
iv = blob[SALT_LENGTH, IV_LENGTH]
@@ -48,7 +51,7 @@ def self.decrypt(encoded_blob, passphrase)
4851
cipher.auth_tag = auth_tag
4952

5053
cipher.update(ciphertext) + cipher.final
51-
rescue OpenSSL::Cipher::CipherError
54+
rescue OpenSSL::Cipher::CipherError, ArgumentError
5255
raise DecryptionError, 'Decryption failed — wrong passphrase or corrupted data'
5356
end
5457

lib/config/project_config.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ def add_project(attrs)
4646
raise ArgumentError, 'Project name is required' if project[:name].to_s.strip.empty?
4747
raise ArgumentError, 'Slack team ID is required' if project[:slack_team_id].to_s.strip.empty?
4848
raise ArgumentError, "Project '#{project[:name]}' already exists" if find_by_name(project[:name])
49+
if find_by_team_id(project[:slack_team_id])
50+
raise ArgumentError, "Team ID '#{project[:slack_team_id]}' already in use"
51+
end
4952

5053
@projects << project
5154
project
@@ -59,6 +62,8 @@ def update_project(name, attrs)
5962
sym_key = key.to_sym
6063
project[sym_key] = value if project.key?(sym_key)
6164
end
65+
66+
validate_project!(project)
6267
project
6368
end
6469

@@ -80,6 +85,14 @@ def empty?
8085

8186
private
8287

88+
def validate_project!(project)
89+
raise ArgumentError, 'Project name is required' if project[:name].to_s.strip.empty?
90+
raise ArgumentError, 'Slack team ID is required' if project[:slack_team_id].to_s.strip.empty?
91+
92+
duplicate = @projects.find { |p| p[:slack_team_id] == project[:slack_team_id] && p != project }
93+
raise ArgumentError, "Team ID '#{project[:slack_team_id]}' already in use" if duplicate
94+
end
95+
8396
def normalize_project(attrs)
8497
{
8598
name: attrs[:name] || attrs['name'],

0 commit comments

Comments
 (0)