Module: Lich::Common::Authentication::CLI

Defined in:
documented/common/authentication/cli.rb

Overview

CLI login handler for character authentication

Handles the CLI login flow: load saved entries, find character, decrypt password, and authenticate with game server.

Class Method Summary collapse

Class Method Details

.decrypt_and_authenticate(char_entry, entry_data) ⇒ Array<String>?

Decrypts the password from a character entry and authenticates with the game server.

Parameters:

  • char_entry (Hash)

    character entry with :username, :password, :char_name, :game_code, :frontend keys and an optional :generator flag for character-generator entry

  • entry_data (Hash)

    full entry data (needed for encryption mode)

Returns:

  • (Array<String>, nil)

    launch data strings if successful, nil on failure



292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
# File 'documented/common/authentication/cli.rb', line 292

def self.decrypt_and_authenticate(char_entry, entry_data)
  # Get encryption mode from YAML
  encryption_mode = (entry_data[:encryption_mode] || 'plaintext').to_sym

  # Decrypt the password
  begin
    plaintext_password = EntryStore.decrypt_password(
      char_entry[:password],
      mode: encryption_mode,
      account_name: char_entry[:username]
    )
  rescue StandardError => e
    Lich.log "error: Failed to decrypt password: #{e.message}"
    return nil
  end

  unless plaintext_password
    Lich.log "error: No password available for character"
    return nil
  end

  # Authenticate with game server
  begin
    auth_data = Authentication.authenticate(
      account: char_entry[:username],
      password: plaintext_password,
      character: char_entry[:char_name],
      game_code: char_entry[:game_code],
      generator: char_entry[:generator] || false
    )

    # Format and return launch data
    LaunchData.prepare(
      auth_data,
      char_entry[:frontend],
      char_entry[:custom_launch],
      char_entry[:custom_launch_dir]
    )
  rescue StandardError => e
    Lich.log "error: Authentication failed: #{e.message}"
    return nil
  end
end

.execute(character_name, game_code: nil, frontend: nil, custom_launch: nil, data_dir: nil) ⇒ Array<String>?

Executes CLI login flow for a specified character

Examples:

launch_data = CLI.execute('MyCharacter', game_code: 'GS3', frontend: 'stormfront', data_dir: '/path/to/data')
# => ["GAME=GS3", "GAMEHOST=eaccess.play.net", ...]

Parameters:

  • character_name (String)

    Character name to login with

  • game_code (String, nil) (defaults to: nil)

    Game code/instance (GS3, GST, DR, etc.)

  • frontend (String, nil) (defaults to: nil)

    Frontend type (stormfront, avalon, wizard)

  • custom_launch (String, nil) (defaults to: nil)

    Custom launch filter (if provided, frontend is ignored for matching)

  • data_dir (String) (defaults to: nil)

    Directory containing saved login entries

Returns:

  • (Array<String>, nil)

    Launch data strings if successful, nil if login fails



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'documented/common/authentication/cli.rb', line 32

def self.execute(character_name, game_code: nil, frontend: nil, custom_launch: nil, data_dir: nil)
  data_dir ||= DATA_DIR

  unless character_name && !character_name.empty?
    Lich.log "error: Character name is required"
    return nil
  end

  entry_data = load_entry_data(data_dir)
  return nil unless entry_data

  char_entry = select_saved_entry(
    entry_data,
    character_name,
    game_code: game_code,
    frontend: frontend,
    custom_launch: custom_launch
  )
  return nil unless char_entry

  # Decrypt password and authenticate
  decrypt_and_authenticate(char_entry, entry_data)
end

.execute_new_character(account_name, game_code: nil, frontend: nil, custom_launch: nil, custom_launch_dir: nil, data_dir: nil) ⇒ Array<String>?

Executes CLI login flow to enter the character generator on an existing account.

Looks up the account by name in saved entries, decrypts its password, and authenticates with character name "NEW" to reach the character creation flow on the game server.

Examples:

launch_data = CLI.execute_new_character('MYACCOUNT', game_code: 'DR', data_dir: '/path/to/data')

Parameters:

  • account_name (String)

    account name as stored in entry.yaml

  • game_code (String, nil) (defaults to: nil)

    game instance code (e.g. "DR", "GS3")

  • frontend (String, Symbol, nil) (defaults to: nil)

    requested frontend for the launched session (nil or the :__unset sentinel default to 'profanity')

  • custom_launch (String, Symbol, nil) (defaults to: nil)

    custom launch command (the :__unset sentinel is treated as none)

  • custom_launch_dir (String, Symbol, nil) (defaults to: nil)

    custom launch directory (the :__unset sentinel is treated as none)

  • data_dir (String, nil) (defaults to: nil)

    directory containing saved login entries

Returns:

  • (Array<String>, nil)

    launch data strings if successful, nil on failure



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# File 'documented/common/authentication/cli.rb', line 125

def self.execute_new_character(, game_code: nil, frontend: nil, custom_launch: nil, custom_launch_dir: nil, data_dir: nil)
  data_dir ||= DATA_DIR

  unless  && !.empty?
    Lich.log "error: Account name is required for new character creation"
    return nil
  end

  unless game_code && LoginHelpers.valid_game_code?(game_code.to_s)
    Lich.log "error: A valid game code is required for new character creation (e.g. --dr, --gemstone). Got: #{game_code.inspect}"
    return nil
  end

  entry_data = load_entry_data(data_dir)
  return nil unless entry_data

  canonical_name,  = (entry_data, )
  return nil unless 

  char_entry = {
    username: canonical_name,
    password: [:password],
    char_name: 'NEW',
    game_code: game_code,
    frontend: (frontend) ? 'profanity' : frontend,
    custom_launch: (custom_launch) ? nil : custom_launch,
    custom_launch_dir: (custom_launch_dir) ? nil : custom_launch_dir,
    generator: true,
  }

  decrypt_and_authenticate(char_entry, entry_data)
end

.find_account(entry_data, account_name) ⇒ Array(String, Hash)?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Finds an account by name in the entry data (case-insensitive).

Returns the stored canonical account key alongside the account data so callers authenticate with the canonical identifier rather than the caller-supplied casing.

Parameters:

  • entry_data (Hash)

    symbolized entry data with :accounts key

  • account_name (String)

    account name to search for

Returns:

  • (Array(String, Hash), nil)

    [canonical account name, account data], or nil if not found



258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# File 'documented/common/authentication/cli.rb', line 258

def self.(entry_data, )
  # New character creation requires the accounts-based YAML format. Legacy
  # array-format entries have no account container to look up by name.
  unless entry_data.is_a?(Hash)
    Lich.log "error: New character creation requires the accounts-based entry format"
    return nil
  end

  accounts = entry_data[:accounts]
  unless accounts.is_a?(Hash)
    Lich.log "error: No accounts found in saved entries"
    return nil
  end

  canonical_name,  = accounts.find { |key, _v| key.to_s.casecmp?() }
  unless 
    Lich.log "error: Account not found: #{}"
    return nil
  end

  unless [:password]
    Lich.log "error: No password saved for account: #{}"
    return nil
  end

  [canonical_name.to_s, ]
end

.load_entry_data(data_dir) ⇒ Hash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Loads and parses entry.yaml from the given data directory.

Parameters:

  • data_dir (String)

    directory containing entry.yaml

Returns:

  • (Hash, nil)

    symbolized entry data, or nil on failure



172
173
174
175
176
177
178
179
# File 'documented/common/authentication/cli.rb', line 172

def self.load_entry_data(data_dir)
  unless CLIPassword.validate_master_password_available(data_dir: data_dir)
    Lich.log "error: Master password validation failed during CLI login"
    return nil
  end

  read_entry_data(data_dir)
end

.load_entry_metadata(data_dir) ⇒ Hash, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Loads saved-entry metadata without requesting password access.

Parameters:

  • data_dir (String)

    directory containing entry.yaml

Returns:

  • (Hash, Array, nil)

    symbolized entry data, or nil on failure



186
187
188
# File 'documented/common/authentication/cli.rb', line 186

def self.(data_dir)
  read_entry_data(data_dir)
end

.read_entry_data(data_dir) ⇒ Hash, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Reads and parses entry.yaml from the given data directory.

Parameters:

  • data_dir (String)

    directory containing entry.yaml

Returns:

  • (Hash, Array, nil)

    symbolized entry data, or nil on failure



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'documented/common/authentication/cli.rb', line 195

def self.read_entry_data(data_dir)
  yaml_file = EntryStore.yaml_file_path(data_dir)
  unless File.exist?(yaml_file)
    Lich.log "error: No saved entries YAML file found"
    return nil
  end

  yaml_data = YAML.safe_load_file(yaml_file, permitted_classes: [Symbol])
  LoginHelpers.symbolize_keys(yaml_data)
rescue StandardError => e
  Lich.log "error: Failed to load YAML data: #{e.message}"
  nil
end

.resolve_saved_target(character_name, game_code: :__unset, frontend: :__unset, custom_launch: :__unset, data_dir: nil) ⇒ Hash?

Resolves a saved character without decrypting its password or authenticating with Simutronics. This is the CLI/TUI boundary for frontends, such as Saga, that own authentication and Lich startup.

Parameters:

  • character_name (String)

    character name to resolve

  • game_code (String, Symbol, nil) (defaults to: :__unset)

    optional game instance filter

  • frontend (String, Symbol, nil) (defaults to: :__unset)

    optional frontend filter

  • custom_launch (String, Symbol, nil) (defaults to: :__unset)

    optional Custom Launch filter

  • data_dir (String, nil) (defaults to: nil)

    directory containing saved login entries

Returns:

  • (Hash, nil)

    password-free saved target, or nil when the store cannot be read or no complete target matches

Raises:

  • (ArgumentError)

    when character_name is blank



68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'documented/common/authentication/cli.rb', line 68

def self.resolve_saved_target(
  character_name,
  game_code: :__unset,
  frontend: :__unset,
  custom_launch: :__unset,
  data_dir: nil
)
  raise ArgumentError, 'Character name is required' if character_name.to_s.strip.empty?

  data_dir ||= DATA_DIR
  entry_data = (data_dir)
  return nil unless entry_data

  char_entry = select_saved_entry(
    entry_data,
    character_name,
    game_code: game_code,
    frontend: frontend,
    custom_launch: custom_launch
  )
  return nil unless char_entry

   = char_entry[:username] || char_entry[:user_id]
  target = {
    account: ,
    character: char_entry[:char_name],
    game_code: char_entry[:game_code],
    frontend: char_entry[:frontend],
    custom_launch: char_entry[:custom_launch]
  }

  missing = target.filter_map { |name, value| name if %i[account character game_code].include?(name) && value.to_s.strip.empty? }
  unless missing.empty?
    Lich.log "error: Saved character is missing required Saga launch data: #{missing.join(', ')}"
    return nil
  end

  target.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
end

.select_saved_entry(entry_data, character_name, game_code:, frontend:, custom_launch:) ⇒ Hash?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Finds and selects one saved entry using the established CLI matching rules.

Parameters:

  • entry_data (Hash, Array)

    symbolized saved-entry data

  • character_name (String)

    character name to match

  • game_code (String, Symbol, nil)

    game instance filter

  • frontend (String, Symbol, nil)

    frontend filter

  • custom_launch (String, Symbol, nil)

    Custom Launch filter

Returns:

  • (Hash, nil)

    selected saved entry, or nil when no match exists



219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'documented/common/authentication/cli.rb', line 219

def self.select_saved_entry(entry_data, character_name, game_code:, frontend:, custom_launch:)
  matching_entries = LoginHelpers.find_character_by_name_game_and_frontend(
    entry_data,
    character_name,
    game_code,
    frontend,
    custom_launch
  )

  if matching_entries.nil? || matching_entries.empty?
    Lich.log "error: No matching character found for: #{character_name}"
    return nil
  end

  char_entry = LoginHelpers.select_best_fit(
    char_data_sets: matching_entries,
    requested_character: character_name,
    requested_instance: game_code,
    requested_fe: frontend
  )

  unless char_entry
    Lich.log "error: Could not select character entry from matches"
    return nil
  end

  char_entry
end

.unset_login_value?(value) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Treats nil and the :__unset CLI sentinel as "value not provided".

Parameters:

  • value (Object)

    a parsed CLI login value

Returns:

  • (Boolean)

    true when the value should be treated as absent



163
164
165
# File 'documented/common/authentication/cli.rb', line 163

def self.(value)
  value.nil? || value == :__unset
end