Module: Asherah

Defined in:
lib/asherah.rb,
lib/asherah/error.rb,
lib/asherah/hooks.rb,
lib/asherah/config.rb,
lib/asherah/native.rb,
lib/asherah/session.rb,
lib/asherah/version.rb,
lib/asherah/session_factory.rb

Defined Under Namespace

Modules: Hooks, Native Classes: Config, Error, Session, SessionFactory

Constant Summary collapse

DEFAULT_SESSION_CACHE_MAX_SIZE =
1000
VERSION =
"0.10.1"

Class Method Summary collapse

Class Method Details

.clear_log_hookObject

Remove the active log hook, if any. Idempotent.



221
222
223
# File 'lib/asherah.rb', line 221

def clear_log_hook
  Hooks.clear_log_hook
end

.clear_metrics_hookObject

Remove the active metrics hook and disable metrics. Idempotent.



239
240
241
# File 'lib/asherah.rb', line 239

def clear_metrics_hook
  Hooks.clear_metrics_hook
end

.configureObject

Configure Asherah using a block with snake_case accessors. Compatible with the canonical godaddy/asherah-ruby gem API.

Asherah.configure do |config|
  config.service_name = "MyService"
  config.product_id = "MyProduct"
  config.kms = "static"
  config.metastore = "memory"
end


40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/asherah.rb', line 40

def configure
  @mutex.synchronize do
    raise Error::AlreadyInitialized if @initialized

    config = Config.new
    yield config
    config.validate!

    json = config.to_json
    pointer = Native.asherah_factory_new_with_config(json)
    @factory = SessionFactory.new(pointer)
    @sessions = {}
    @initialized = true
    @session_cache_enabled = config.enable_session_caching != false
    @session_cache_max_size = positive_int(config.session_cache_max_size) || DEFAULT_SESSION_CACHE_MAX_SIZE
    @verbose = config.verbose == true
  end
end

.decrypt(partition_id, data_row_record) ⇒ Object

Raises:

  • (ArgumentError)


176
177
178
179
180
# File 'lib/asherah.rb', line 176

def decrypt(partition_id, data_row_record)
  raise ArgumentError, "data_row_record cannot be nil" if data_row_record.nil?
  session = resolve_session(partition_id)
  session.decrypt_bytes(data_row_record).force_encoding(Encoding::UTF_8)
end

.decrypt_async(partition_id, data_row_record, &block) ⇒ Object

Run decrypt in a background Thread; see ‘setup_async` for the exception-propagation contract.



200
201
202
203
204
205
206
207
# File 'lib/asherah.rb', line 200

def decrypt_async(partition_id, data_row_record, &block)
  Thread.new do
    Thread.current.report_on_exception = true
    result = decrypt(partition_id, data_row_record)
    block&.call(result)
    result
  end
end

.decrypt_string(partition_id, data_row_record) ⇒ Object

Raises:

  • (ArgumentError)


182
183
184
185
# File 'lib/asherah.rb', line 182

def decrypt_string(partition_id, data_row_record)
  raise ArgumentError, "data_row_record cannot be nil" if data_row_record.nil?
  decrypt(partition_id, data_row_record).force_encoding(Encoding::UTF_8)
end

.encrypt(partition_id, payload) ⇒ Object

Raises:

  • (ArgumentError)


165
166
167
168
169
# File 'lib/asherah.rb', line 165

def encrypt(partition_id, payload)
  raise ArgumentError, "payload cannot be nil" if payload.nil?
  session = resolve_session(partition_id)
  session.encrypt_bytes(payload)
end

.encrypt_async(partition_id, payload, &block) ⇒ Object

Run encrypt in a background Thread; see ‘setup_async` for the exception-propagation contract.



189
190
191
192
193
194
195
196
# File 'lib/asherah.rb', line 189

def encrypt_async(partition_id, payload, &block)
  Thread.new do
    Thread.current.report_on_exception = true
    result = encrypt(partition_id, payload)
    block&.call(result)
    result
  end
end

.encrypt_string(partition_id, text) ⇒ Object

Raises:

  • (ArgumentError)


171
172
173
174
# File 'lib/asherah.rb', line 171

def encrypt_string(partition_id, text)
  raise ArgumentError, "text cannot be nil" if text.nil?
  encrypt(partition_id, text)
end

.get_setup_statusObject



140
141
142
# File 'lib/asherah.rb', line 140

def get_setup_status
  @mutex.synchronize { @initialized }
end

.set_log_hook(callback = nil, &block) ⇒ Object

Install a log hook. Yields a Hash {level:, target:, message:} for every log record emitted by the underlying Rust crates. The block may fire from any thread; implementations must be thread-safe and non-blocking. Pass nil to clear (equivalent to clear_log_hook).

Replaces any previously installed log hook. Exceptions raised from the callback are caught and silently swallowed.



216
217
218
# File 'lib/asherah.rb', line 216

def set_log_hook(callback = nil, &block)
  Hooks.set_log_hook(callback, &block)
end

.set_metrics_hook(callback = nil, &block) ⇒ Object

Install a metrics hook. Yields a Hash {type:, duration_ns:, name:} for every metrics event. Timing events (:decrypt, :store, :load) carry a positive duration_ns and a nil name; cache events (:cache_miss, :cache_stale) carry duration_ns == 0 and the cache identifier in name.

Installing a hook implicitly enables the global metrics gate; clearing it disables the gate. Replaces any previously installed metrics hook. Pass nil to clear (equivalent to clear_metrics_hook).



234
235
236
# File 'lib/asherah.rb', line 234

def set_metrics_hook(callback = nil, &block)
  Hooks.set_metrics_hook(callback, &block)
end

.setenv(env = {}) ⇒ Object Also known as: set_env



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# File 'lib/asherah.rb', line 144

def setenv(env = {})
  data = case env
         when String
           JSON.parse(env)
         else
           env
         end
  unless data.respond_to?(:each_pair)
    raise ArgumentError, "environment payload must be a Hash or JSON object"
  end
  data.each_pair do |k, v|
    if v.nil?
      ENV.delete(String(k))
    else
      ENV[String(k)] = v.to_s
    end
  end
  nil
end

.setup(config) ⇒ Object

Initialize Asherah with a PascalCase config hash. Also accepts snake_case string/symbol keys (auto-normalized).



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/asherah.rb', line 61

def setup(config)
  normalized = normalize_config(config)
  json = JSON.generate(normalized)

  pointer = Native.asherah_factory_new_with_config(json)
  factory = SessionFactory.new(pointer)

  @mutex.synchronize do
    raise Error::AlreadyInitialized if @initialized

    @factory = factory
    @sessions = {}
    @initialized = true
    @session_cache_enabled = truthy(normalized["EnableSessionCaching"], default: true)
    @session_cache_max_size = positive_int(normalized["SessionCacheMaxSize"]) || DEFAULT_SESSION_CACHE_MAX_SIZE
    @verbose = truthy(normalized["Verbose"], default: false)
  end

  nil
rescue StandardError
  factory&.close if defined?(factory) && factory
  raise
end

.setup_async(config, &block) ⇒ Object

Run setup in a background Thread. Yields the result to block on success. Exceptions raised by setup propagate via the Thread’s joined error — the previous implementation called the block with the result on success but silently dropped the Thread’s error state on failure, leaving callers unable to distinguish completion from an unsetup factory. The Thread aborts on exception (‘Thread.report_on_exception = true`), so a stack trace lands on stderr; callers that need programmatic access should `thread.join` to re-raise. T-finding “setup_async/shutdown_async/ encrypt_async/decrypt_async swallow Thread exceptions” in `docs/review-2026-05-05-findings.md`.



96
97
98
99
100
101
102
103
# File 'lib/asherah.rb', line 96

def setup_async(config, &block)
  Thread.new do
    Thread.current.report_on_exception = true
    result = setup(config)
    block&.call(result)
    result
  end
end

.shutdownObject



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/asherah.rb', line 105

def shutdown
  factory = nil
  sessions = nil
  @mutex.synchronize do
    raise Error::NotInitialized unless @initialized

    factory = @factory
    sessions = @sessions.values
    @factory = nil
    @sessions = {}
    @initialized = false
  end

  Array(sessions).each do |session|
    begin
      session.close unless session.closed?
    rescue StandardError => e
      warn "asherah: error closing session during shutdown: #{e.message}"
    end
  end
  factory&.close unless factory&.closed?
  nil
end

.shutdown_async(&block) ⇒ Object

Run shutdown in a background Thread; see ‘setup_async` for the exception-propagation contract.



131
132
133
134
135
136
137
138
# File 'lib/asherah.rb', line 131

def shutdown_async(&block)
  Thread.new do
    Thread.current.report_on_exception = true
    result = shutdown
    block&.call(result)
    result
  end
end