Module: RubynCode::Auth::TokenStore

Defined in:
lib/rubyn_code/auth/token_store.rb

Overview

rubocop:disable Metrics/ModuleLength – single-responsibility credential store

Constant Summary collapse

EXPIRY_BUFFER_SECONDS =

5 minutes

300
KEYCHAIN_SERVICE =
'Claude Code-credentials'

Class Method Summary collapse

Class Method Details

.access_tokenObject



80
# File 'lib/rubyn_code/auth/token_store.rb', line 80

def access_token = self.load&.fetch(:access_token, nil)

.clear!Object

rubocop:disable Naming/PredicateMethod – destructive action, not a predicate



65
66
67
68
# File 'lib/rubyn_code/auth/token_store.rb', line 65

def clear! # rubocop:disable Naming/PredicateMethod -- destructive action, not a predicate
  FileUtils.rm_f(tokens_path)
  true
end

.exists?Boolean

Returns:

  • (Boolean)


79
# File 'lib/rubyn_code/auth/token_store.rb', line 79

def exists? = valid?

.loadObject

Load tokens with fallback chain:

  1. macOS Keychain (Claude Code’s OAuth token)

  2. Local YAML file (~/.rubyn-code/tokens.yml)

  3. ANTHROPIC_API_KEY environment variable



19
20
21
# File 'lib/rubyn_code/auth/token_store.rb', line 19

def load
  load_from_keychain || load_from_file || load_from_env
end

.load_for_provider(provider) ⇒ Object

Load API key for a given provider. Anthropic uses the full fallback chain. Other providers: stored key → env var.



25
26
27
28
29
30
31
32
33
34
# File 'lib/rubyn_code/auth/token_store.rb', line 25

def load_for_provider(provider)
  return load if provider == 'anthropic'

  stored = load_provider_key(provider)
  return { access_token: stored, type: :api_key, source: :stored } if stored

  env_key = resolve_env_key(provider)
  api_key = ENV.fetch(env_key, nil)
  api_key&.empty? == false ? { access_token: api_key, type: :api_key, source: :env } : nil
end

.load_provider_key(provider) ⇒ Object

Retrieve a stored API key for a provider (decrypted transparently).



46
47
48
49
50
51
52
53
# File 'lib/rubyn_code/auth/token_store.rb', line 46

def load_provider_key(provider)
  data = load_tokens_file
  value = data&.dig('provider_keys', provider.to_s)
  return nil unless value

  migrate_plaintext_key!(data, provider, value) unless KeyEncryption.encrypted?(value)
  KeyEncryption.decrypt(value)
end

.save(access_token:, refresh_token:, expires_at:) ⇒ Object



55
56
57
58
59
60
61
62
63
# File 'lib/rubyn_code/auth/token_store.rb', line 55

def save(access_token:, refresh_token:, expires_at:)
  ensure_directory!
  data = load_tokens_file || {}
  data['access_token'] = access_token
  data['refresh_token'] = refresh_token
  data['expires_at'] = expires_at.is_a?(Time) ? expires_at.iso8601 : expires_at.to_s
  write_tokens_file(data)
  data
end

.save_provider_key(provider, key) ⇒ Object

Store an API key for a provider in tokens.yml (encrypted at rest).



37
38
39
40
41
42
43
# File 'lib/rubyn_code/auth/token_store.rb', line 37

def save_provider_key(provider, key)
  ensure_directory!
  data = load_tokens_file || {}
  data['provider_keys'] ||= {}
  data['provider_keys'][provider.to_s] = KeyEncryption.encrypt(key)
  write_tokens_file(data)
end

.valid?Boolean

Returns:

  • (Boolean)


70
71
72
73
74
75
76
77
# File 'lib/rubyn_code/auth/token_store.rb', line 70

def valid?
  tokens = self.load
  return false unless tokens&.fetch(:access_token, nil)
  return true if tokens[:type] == :api_key
  return true unless tokens[:expires_at]

  tokens[:expires_at] > Time.now + EXPIRY_BUFFER_SECONDS
end