asherah 0.10.0-x86_64-linux-musl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ae2e01ccf73a40eae6f7cf5d2f653422569d352cfdfee15795d5603c49c291bd
4
+ data.tar.gz: 10d62fa707459b137e1c7f78727decbba492ef61563a5ff4642e9d0ab1340498
5
+ SHA512:
6
+ metadata.gz: fabdee0a35b921e8ec2847697d5657b188eb7e2be436eeab99fefc34000ccf442b2d6ae24cc1878e792706d9a1903e305c0a83874655747359dc23e10c98c571
7
+ data.tar.gz: c0f7d451fdbeafce00d5903bccf112ca0b43c541d16c5e34da460df53fbe5476e2e0f3d0de1c1e64cf130de496db38843aa7b5eace8ccd909d4eaa39ab7ead1c
data/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # asherah
2
+
3
+ Ruby bindings for [Asherah](https://github.com/godaddy/asherah-ffi) envelope encryption with automatic key rotation.
4
+
5
+ Published to [RubyGems](https://rubygems.org/gems/asherah) with prebuilt native libraries for Linux x64/ARM64 (glibc and musl/Alpine) and macOS x64/ARM64. A fallback source gem is available for other platforms (requires the Rust toolchain to compile).
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ gem install asherah
11
+ ```
12
+
13
+ Or add to your Gemfile:
14
+
15
+ ```ruby
16
+ gem 'asherah'
17
+ ```
18
+
19
+ The gem uses FFI to load the native Asherah library. Platform-specific gems ship the prebuilt library; the source gem builds it during installation.
20
+
21
+ ## Documentation
22
+
23
+ Task-oriented walkthroughs under [`docs/`](./docs/):
24
+
25
+ | Guide | When to read |
26
+ |---|---|
27
+ | [Getting started](./docs/getting-started.md) | `gem install` through round-trip encrypt/decrypt. |
28
+ | [Framework integration](./docs/framework-integration.md) | Rails, Sidekiq, Sinatra, Rack middleware, AWS Lambda. |
29
+ | [AWS production setup](./docs/aws-production-setup.md) | KMS keys, DynamoDB, IAM policy, region routing. |
30
+ | [Testing](./docs/testing.md) | RSpec/Minitest fixtures, Testcontainers, mocking patterns. |
31
+ | [Troubleshooting](./docs/troubleshooting.md) | Common errors with what to check first. |
32
+
33
+ ## Quick Start
34
+
35
+ The simplest way to use Asherah is the static module API. Call `setup` once at startup and `shutdown` on exit:
36
+
37
+ ```ruby
38
+ require "asherah"
39
+
40
+ Asherah.setup(
41
+ "ServiceName" => "my-service",
42
+ "ProductID" => "my-product",
43
+ "Metastore" => "memory", # testing only
44
+ "KMS" => "static" # testing only
45
+ )
46
+
47
+ ciphertext = Asherah.encrypt_string("partition-id", "sensitive data")
48
+ plaintext = Asherah.decrypt_string("partition-id", ciphertext)
49
+
50
+ Asherah.shutdown
51
+ ```
52
+
53
+ The static API manages a session cache internally. Sessions are created on first use per partition and reused for subsequent calls.
54
+
55
+ ### Block-style configuration
56
+
57
+ For an API compatible with the canonical GoDaddy Asherah Ruby gem, use `configure` with a block:
58
+
59
+ ```ruby
60
+ Asherah.configure do |config|
61
+ config.service_name = "my-service"
62
+ config.product_id = "my-product"
63
+ config.kms = "static" # testing only
64
+ config.metastore = "memory" # testing only
65
+ end
66
+
67
+ ciphertext = Asherah.encrypt_string("partition-id", "sensitive data")
68
+ plaintext = Asherah.decrypt_string("partition-id", ciphertext)
69
+
70
+ Asherah.shutdown
71
+ ```
72
+
73
+ ## Session-Based API
74
+
75
+ For direct control over session lifecycle, use `SessionFactory` and `Session`:
76
+
77
+ ```ruby
78
+ require "asherah"
79
+
80
+ Asherah.configure do |config|
81
+ config.service_name = "my-service"
82
+ config.product_id = "my-product"
83
+ config.kms = "static" # testing only
84
+ config.metastore = "memory" # testing only
85
+ end
86
+
87
+ factory = Asherah::SessionFactory.new(
88
+ Asherah::Native.asherah_factory_new_with_config(config_json)
89
+ )
90
+ session = factory.get_session("partition-id")
91
+
92
+ ciphertext = session.encrypt_bytes("sensitive data")
93
+ plaintext = session.decrypt_bytes(ciphertext)
94
+
95
+ session.close
96
+ factory.close
97
+ ```
98
+
99
+ Or via the static API's internal factory (the typical pattern):
100
+
101
+ ```ruby
102
+ Asherah.setup("ServiceName" => "my-service", "ProductID" => "my-product",
103
+ "Metastore" => "memory", "KMS" => "static") # testing only
104
+
105
+ # The static API acquires and caches sessions automatically
106
+ ct = Asherah.encrypt("partition-id", "data")
107
+ pt = Asherah.decrypt("partition-id", ct)
108
+
109
+ Asherah.shutdown
110
+ ```
111
+
112
+ ## Async API
113
+
114
+ ### Session-level async (true async via Rust tokio)
115
+
116
+ The session's async methods dispatch work to Rust's tokio runtime and receive results via FFI callbacks:
117
+
118
+ ```ruby
119
+ session = factory.get_session("partition-id")
120
+
121
+ ct = session.encrypt_bytes_async(data)
122
+ pt = session.decrypt_bytes_async(ct)
123
+
124
+ session.close
125
+ ```
126
+
127
+ ### Static-level async (thread-based)
128
+
129
+ The static API's async methods run in a Ruby `Thread`:
130
+
131
+ ```ruby
132
+ thread = Asherah.encrypt_async("partition-id", data) do |result|
133
+ puts "Encrypted: #{result.bytesize} bytes"
134
+ end
135
+ thread.join
136
+ ```
137
+
138
+ ### Async Behavior
139
+
140
+ The session-level async methods (`encrypt_bytes_async`, `decrypt_bytes_async`) are true async. The encrypt/decrypt work runs on Rust's tokio worker threads and completes via an FFI callback. The Ruby interpreter is NOT blocked during the native call.
141
+
142
+ However, the implementation uses `Queue#pop` to synchronize the callback result back to the calling Ruby thread. This means `queue.pop` blocks the calling Ruby thread until the result arrives. True concurrency requires multiple Ruby threads or Ractors dispatching async calls in parallel.
143
+
144
+ If the FFI callback never fires (e.g. the worker pool deadlocks), the
145
+ async call raises `Asherah::Error::Timeout` after 30 seconds rather
146
+ than blocking indefinitely. Override the bound by setting the
147
+ `ASHERAH_RUBY_ASYNC_TIMEOUT` environment variable (in seconds) before
148
+ the gem is loaded.
149
+
150
+ The static-level async methods (`Asherah.encrypt_async`, `Asherah.decrypt_async`) simply run the sync operation in a new `Thread`.
151
+
152
+ ## Input contract
153
+
154
+ **Partition ID** (`nil`, `""`): always rejected as programming errors
155
+ with `ArgumentError` (`"partition_id cannot be empty"`). No row is ever
156
+ written to the metastore under a degenerate partition ID.
157
+
158
+ **Plaintext** to encrypt:
159
+ - `nil` → `ArgumentError` from explicit guards in the public API before
160
+ any FFI call.
161
+ - Empty `String` (`""`) and empty bytes (`"".b`) are **valid**
162
+ plaintexts. `Asherah.encrypt_string` / `session.encrypt_bytes`
163
+ produce a real `DataRowRecord` envelope; the matching decrypt returns
164
+ exactly `""` or empty bytes.
165
+
166
+ **Ciphertext** to decrypt:
167
+ - `nil` → `ArgumentError`.
168
+ - Empty `String` → `Asherah::Error::DecryptFailed` (not valid
169
+ `DataRowRecord` JSON).
170
+
171
+ **Do not short-circuit empty plaintext encryption in caller code** —
172
+ empty data is real data, encrypting it produces a genuine envelope, and
173
+ skipping encryption leaks the fact that the value was empty. See
174
+ [docs/input-contract.md](../docs/input-contract.md) for the full
175
+ rationale.
176
+
177
+ ## Migration from Canonical Ruby SDK
178
+
179
+ This replaces the original `asherah` gem which was built on Go via Cobhan FFI. The API is drop-in compatible:
180
+
181
+ | | Canonical (Go/Cobhan) | This binding (Rust/FFI) |
182
+ |---|---|---|
183
+ | Implementation | Go + Cobhan FFI | Rust + Ruby FFI gem |
184
+ | `Asherah.configure` | Supported | Supported (same API) |
185
+ | `Asherah.encrypt` / `decrypt` | Supported | Supported (same API) |
186
+ | `SessionFactory` | Supported | Supported (same API) |
187
+ | Memory protection | None | memguard (locked, wiped pages) |
188
+ | Async support | None | Session-level true async |
189
+
190
+ Migration steps:
191
+ 1. Update the `asherah` gem version in your Gemfile
192
+ 2. No code changes required -- the API is compatible
193
+ 3. Both read the same metastore tables -- no data migration required
194
+
195
+ ## Performance
196
+
197
+ Benchmarked on Apple M4 Max, 64-byte payload, hot session cache:
198
+
199
+ | Operation | Latency |
200
+ |---|---|
201
+ | Encrypt | ~1,170 ns |
202
+ | Decrypt | ~1,110 ns |
203
+
204
+ ## Configuration
205
+
206
+ ### `setup` (hash style)
207
+
208
+ Keys are PascalCase strings matching the Asherah configuration format:
209
+
210
+ | Key | Type | Required | Description |
211
+ |---|---|---|---|
212
+ | `ServiceName` | `String` | Yes | Service identifier for key hierarchy |
213
+ | `ProductID` | `String` | Yes | Product identifier for key hierarchy |
214
+ | `Metastore` | `String` | Yes | `"rdbms"`, `"dynamodb"`, `"memory"` (testing) |
215
+ | `KMS` | `String` | Yes | `"static"` or `"aws"` |
216
+ | `ConnectionString` | `String` | No | RDBMS connection string |
217
+ | `DynamoDBEndpoint` | `String` | No | Custom DynamoDB endpoint |
218
+ | `DynamoDBRegion` | `String` | No | DynamoDB region — drives endpoint URL resolution and (when `DynamoDBSigningRegion` is unset) SigV4 signing |
219
+ | `DynamoDBSigningRegion` | `String` | No | SigV4 signing region. When set distinct from `DynamoDBRegion`, the URL is built from `DynamoDBRegion` but SigV4 signs as `DynamoDBSigningRegion` |
220
+ | `DynamoDBTableName` | `String` | No | DynamoDB table name |
221
+ | `RegionMap` | `Hash` | No | AWS KMS region-to-ARN map |
222
+ | `PreferredRegion` | `String` | No | Preferred AWS KMS region |
223
+ | `AwsProfileName` | `String` | No | AWS shared-credentials profile for KMS, DynamoDB, and Secrets Manager clients (native Rust SDK) |
224
+ | `EnableRegionSuffix` | `Boolean` | No | Append region suffix to key IDs |
225
+ | `EnableSessionCaching` | `Boolean` | No | Enable session caching (default: true) |
226
+ | `SessionCacheMaxSize` | `Integer` | No | Max cached sessions |
227
+ | `SessionCacheDuration` | `Integer` | No | Cache TTL in milliseconds |
228
+ | `ExpireAfter` | `Integer` | No | Key expiration in seconds |
229
+ | `CheckInterval` | `Integer` | No | Key check interval in seconds |
230
+ | `Verbose` | `Boolean` | No | Enable verbose logging (default: false) |
231
+ | `PoolMaxOpen` | `Integer` | No | Max open DB connections (default: 0 = unlimited) |
232
+ | `PoolMaxIdle` | `Integer` | No | Max idle connections to retain (default: 2) |
233
+ | `PoolMaxLifetime` | `Integer` | No | Max connection lifetime in seconds (default: 0 = unlimited) |
234
+ | `PoolMaxIdleTime` | `Integer` | No | Max idle time per connection in seconds (default: 0 = unlimited) |
235
+
236
+ For AWS KMS, DynamoDB, or Secrets Manager, when `AwsProfileName` is omitted the native Rust credential chain applies (including `AWS_PROFILE` and shared config under `~/.aws/`). Setting `AwsProfileName` / `aws_profile_name` selects a named profile explicitly.
237
+
238
+ ### `configure` (block style)
239
+
240
+ Uses snake_case attribute accessors:
241
+
242
+ | Attribute | Maps to |
243
+ |---|---|
244
+ | `service_name` | `ServiceName` |
245
+ | `product_id` | `ProductID` |
246
+ | `metastore` | `Metastore` |
247
+ | `kms` | `KMS` |
248
+ | `connection_string` | `ConnectionString` |
249
+ | `dynamo_db_endpoint` | `DynamoDBEndpoint` |
250
+ | `dynamo_db_region` | `DynamoDBRegion` |
251
+ | `dynamo_db_signing_region` | `DynamoDBSigningRegion` |
252
+ | `dynamo_db_table_name` | `DynamoDBTableName` |
253
+ | `region_map` | `RegionMap` |
254
+ | `preferred_region` | `PreferredRegion` |
255
+ | `aws_profile_name` | `AwsProfileName` |
256
+ | `enable_region_suffix` | `EnableRegionSuffix` |
257
+ | `enable_session_caching` | `EnableSessionCaching` |
258
+ | `session_cache_max_size` | `SessionCacheMaxSize` |
259
+ | `session_cache_duration` | `SessionCacheDuration` |
260
+ | `expire_after` | `ExpireAfter` |
261
+ | `check_interval` | `CheckInterval` |
262
+ | `verbose` | `Verbose` |
263
+ | `pool_max_open` | `PoolMaxOpen` |
264
+ | `pool_max_idle` | `PoolMaxIdle` |
265
+ | `pool_max_lifetime` | `PoolMaxLifetime` |
266
+ | `pool_max_idle_time` | `PoolMaxIdleTime` |
267
+
268
+ ## API Reference
269
+
270
+ ### `Asherah` (module-level static API)
271
+
272
+ | Method | Description |
273
+ |---|---|
274
+ | `setup(config_hash)` | Initialize with PascalCase config hash |
275
+ | `configure { \|c\| ... }` | Initialize with block-style snake_case config |
276
+ | `setup_async(config_hash, &block)` | Async `setup` in a Thread |
277
+ | `shutdown` | Release all resources and cached sessions |
278
+ | `shutdown_async(&block)` | Async `shutdown` in a Thread |
279
+ | `get_setup_status` | Returns `true` if initialized |
280
+ | `encrypt(partition, data)` | Encrypt bytes, returns DRR JSON bytes |
281
+ | `encrypt_string(partition, text)` | Encrypt string, returns DRR JSON string |
282
+ | `encrypt_async(partition, data, &block)` | Encrypt in a Thread |
283
+ | `decrypt(partition, drr)` | Decrypt DRR JSON bytes to plaintext |
284
+ | `decrypt_string(partition, drr)` | Decrypt DRR JSON string to plaintext string |
285
+ | `decrypt_async(partition, drr, &block)` | Decrypt in a Thread |
286
+ | `setenv(hash)` / `set_env(hash)` | Set environment variables |
287
+
288
+ ### `Asherah::SessionFactory`
289
+
290
+ | Method | Description |
291
+ |---|---|
292
+ | `get_session(partition_id)` | Create a session for a partition |
293
+ | `close` | Release the factory |
294
+ | `closed?` | Returns `true` if closed |
295
+
296
+ ### `Asherah::Session`
297
+
298
+ | Method | Description |
299
+ |---|---|
300
+ | `encrypt_bytes(data)` | Encrypt bytes, returns DRR JSON bytes |
301
+ | `decrypt_bytes(json)` | Decrypt DRR JSON bytes to plaintext bytes |
302
+ | `encrypt_bytes_async(data)` | True async encrypt via Rust tokio |
303
+ | `decrypt_bytes_async(json)` | True async decrypt via Rust tokio |
304
+ | `close` | Release the session |
305
+ | `closed?` | Returns `true` if closed |
306
+
307
+ ## Observability hooks
308
+
309
+ ### Log hook
310
+
311
+ Asherah ships first-class stdlib `Logger` integration. The simplest way to
312
+ wire up logging is to hand it any Logger-compatible instance — stdlib
313
+ `Logger`, `ActiveSupport::Logger`, `SemanticLogger`, `Ougai`, etc. — and the
314
+ bridge dispatches each record via `Logger#add(severity, message, target)`
315
+ so the logger's own filter rules and formatters apply.
316
+
317
+ ```ruby
318
+ require "logger"
319
+ log = Logger.new($stdout)
320
+ log.level = Logger::WARN
321
+ Asherah.set_log_hook(log)
322
+
323
+ # ...later
324
+ Asherah.clear_log_hook
325
+ ```
326
+
327
+ For raw access pass a block; the event is a `Hash` with both a
328
+ `Logger::Severity` integer and a matching lowercase symbol:
329
+
330
+ ```ruby
331
+ Asherah.set_log_hook do |event|
332
+ # event[:severity] => Logger::DEBUG | INFO | WARN | ERROR
333
+ # event[:level] => :debug | :info | :warn | :error (symbol, for case dispatch)
334
+ # event[:target] => "asherah::session"
335
+ # event[:message] => "..."
336
+ next if event[:severity] < Logger::WARN
337
+ warn "[asherah #{event[:level]}] #{event[:target]}: #{event[:message]}"
338
+ end
339
+ ```
340
+
341
+ The Rust `log` crate has a TRACE level that stdlib `Logger` does not; Asherah
342
+ maps `trace` records to `Logger::DEBUG` so the value is still meaningful.
343
+ The block may fire from any thread (Rust tokio worker threads, DB driver
344
+ threads), so implementations must be thread-safe and should not block.
345
+ Exceptions raised from the callback are caught and silently swallowed —
346
+ propagating an exception across the FFI boundary is undefined behavior.
347
+
348
+ ### Metrics hook
349
+
350
+ Receive timing observations (`:encrypt`, `:decrypt`, `:store`, `:load`) and
351
+ cache events (`:cache_hit`, `:cache_miss`, `:cache_stale`) via
352
+ `Asherah.set_metrics_hook`. Installing a hook implicitly enables the global
353
+ metrics gate; clearing it disables the gate.
354
+
355
+ ```ruby
356
+ Asherah.set_metrics_hook do |event|
357
+ case event[:type]
358
+ when :encrypt, :decrypt, :store, :load
359
+ # event[:duration_ns] is the elapsed time in nanoseconds, event[:name] is nil
360
+ Statsd.timing("asherah.#{event[:type]}", event[:duration_ns] / 1_000_000.0)
361
+ when :cache_hit, :cache_miss, :cache_stale
362
+ # event[:name] is the cache identifier, event[:duration_ns] is 0
363
+ Statsd.increment("asherah.#{event[:type]}.#{event[:name]}")
364
+ end
365
+ end
366
+
367
+ # ...later
368
+ Asherah.clear_metrics_hook
369
+ ```
370
+
371
+ | Event type | `:duration_ns` | `:name` |
372
+ |---|---|---|
373
+ | `:encrypt` | elapsed ns | `nil` |
374
+ | `:decrypt` | elapsed ns | `nil` |
375
+ | `:store` | elapsed ns | `nil` |
376
+ | `:load` | elapsed ns | `nil` |
377
+ | `:cache_hit` | `0` | cache identifier |
378
+ | `:cache_miss` | `0` | cache identifier |
379
+ | `:cache_stale` | `0` | cache identifier |
380
+
381
+ The same threading caveats apply as for the log hook — implementations must
382
+ be thread-safe and non-blocking, and exceptions are caught.
383
+
384
+ ## License
385
+
386
+ Licensed under the Apache License, Version 2.0.
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Asherah
6
+ # Configuration class compatible with the canonical godaddy/asherah-ruby gem.
7
+ # Provides snake_case attr_accessors that map to PascalCase config keys
8
+ # expected by the Rust FFI layer.
9
+ #
10
+ # Usage:
11
+ # Asherah.configure do |config|
12
+ # config.service_name = "MyService"
13
+ # config.product_id = "MyProduct"
14
+ # config.kms = "static"
15
+ # config.metastore = "memory"
16
+ # end
17
+ class Config
18
+ MAPPING = {
19
+ service_name: :ServiceName,
20
+ product_id: :ProductID,
21
+ kms: :KMS,
22
+ metastore: :Metastore,
23
+ connection_string: :ConnectionString,
24
+ replica_read_consistency: :ReplicaReadConsistency,
25
+ sql_metastore_db_type: :SQLMetastoreDBType,
26
+ dynamo_db_endpoint: :DynamoDBEndpoint,
27
+ dynamo_db_region: :DynamoDBRegion,
28
+ dynamo_db_signing_region: :DynamoDBSigningRegion,
29
+ dynamo_db_table_name: :DynamoDBTableName,
30
+ enable_region_suffix: :EnableRegionSuffix,
31
+ region_map: :RegionMap,
32
+ preferred_region: :PreferredRegion,
33
+ aws_profile_name: :AwsProfileName,
34
+ session_cache_max_size: :SessionCacheMaxSize,
35
+ session_cache_duration: :SessionCacheDuration,
36
+ enable_session_caching: :EnableSessionCaching,
37
+ disable_zero_copy: :DisableZeroCopy,
38
+ expire_after: :ExpireAfter,
39
+ check_interval: :CheckInterval,
40
+ verbose: :Verbose,
41
+ # Connection pool
42
+ pool_max_open: :PoolMaxOpen,
43
+ pool_max_idle: :PoolMaxIdle,
44
+ pool_max_lifetime: :PoolMaxLifetime,
45
+ pool_max_idle_time: :PoolMaxIdleTime,
46
+ # KMS: AWS
47
+ kms_key_id: :KmsKeyId,
48
+ # KMS: AWS Secrets Manager
49
+ secrets_manager_secret_id: :SecretsManagerSecretId,
50
+ # KMS: HashiCorp Vault Transit
51
+ vault_addr: :VaultAddr,
52
+ vault_token: :VaultToken,
53
+ vault_auth_method: :VaultAuthMethod,
54
+ vault_auth_role: :VaultAuthRole,
55
+ vault_auth_mount: :VaultAuthMount,
56
+ vault_approle_role_id: :VaultApproleRoleId,
57
+ vault_approle_secret_id: :VaultApproleSecretId,
58
+ vault_client_cert: :VaultClientCert,
59
+ vault_client_key: :VaultClientKey,
60
+ vault_k8s_token_path: :VaultK8sTokenPath,
61
+ vault_transit_key: :VaultTransitKey,
62
+ vault_transit_mount: :VaultTransitMount,
63
+ }.freeze
64
+
65
+ KMS_TYPES = ["static", "aws", "vault", "vault-transit", "secrets-manager", "test-debug-static"].freeze
66
+ METASTORE_TYPES = ["rdbms", "dynamodb", "memory", "test-debug-memory"].freeze
67
+ SQL_METASTORE_DB_TYPES = ["mysql", "postgres", "oracle"].freeze
68
+
69
+ attr_accessor(*MAPPING.keys)
70
+
71
+ def validate!
72
+ raise Error::ConfigError, "config.service_name not set" if service_name.nil?
73
+ raise Error::ConfigError, "config.product_id not set" if product_id.nil?
74
+ raise Error::ConfigError, "config.kms not set" if kms.nil?
75
+ unless KMS_TYPES.include?(kms)
76
+ raise Error::ConfigError, "config.kms must be one of these: #{KMS_TYPES.join(", ")}"
77
+ end
78
+ raise Error::ConfigError, "config.metastore not set" if metastore.nil?
79
+ unless METASTORE_TYPES.include?(metastore)
80
+ raise Error::ConfigError, "config.metastore must be one of these: #{METASTORE_TYPES.join(", ")}"
81
+ end
82
+ if sql_metastore_db_type && !SQL_METASTORE_DB_TYPES.include?(sql_metastore_db_type)
83
+ raise Error::ConfigError, "config.sql_metastore_db_type must be one of these: #{SQL_METASTORE_DB_TYPES.join(", ")}"
84
+ end
85
+ if kms == "aws"
86
+ raise Error::ConfigError, "config.region_map not set" if region_map.nil?
87
+ raise Error::ConfigError, "config.region_map must be a Hash" unless region_map.is_a?(Hash)
88
+ raise Error::ConfigError, "config.preferred_region not set" if preferred_region.nil?
89
+ end
90
+ end
91
+
92
+ def to_json(*args)
93
+ JSON.generate(to_h, *args)
94
+ end
95
+
96
+ # Convert to the PascalCase Hash expected by Asherah.setup
97
+ def to_h
98
+ hash = {}
99
+ MAPPING.each_pair do |attr, key|
100
+ value = public_send(attr)
101
+ hash[key] = value unless value.nil?
102
+ end
103
+ hash
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asherah
4
+ # Base error class. Also serves as a namespace for specific error types
5
+ # compatible with the canonical godaddy/asherah-ruby gem.
6
+ class Error < StandardError
7
+ ConfigError = Class.new(self)
8
+ NotInitialized = Class.new(self)
9
+ AlreadyInitialized = Class.new(self)
10
+ GetSessionFailed = Class.new(self)
11
+ EncryptFailed = Class.new(self)
12
+ DecryptFailed = Class.new(self)
13
+ BadConfig = Class.new(self)
14
+ Timeout = Class.new(self)
15
+ end
16
+ end