fluyenta-ruby 0.1.14

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.
Files changed (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Utilities
5
+ # Rate limiter with support for sliding window and token bucket algorithms
6
+ # Integrates with Flux for metrics tracking
7
+ #
8
+ # @example Basic usage
9
+ # limiter = BrainzLab::Utilities::RateLimiter.new(
10
+ # key: "api:user:123",
11
+ # limit: 100,
12
+ # window: 60 # seconds
13
+ # )
14
+ #
15
+ # if limiter.allow?
16
+ # # proceed with request
17
+ # else
18
+ # # rate limited
19
+ # end
20
+ #
21
+ # @example With block
22
+ # BrainzLab::Utilities::RateLimiter.throttle("api:user:#{user.id}", limit: 100, window: 60) do
23
+ # # this block runs only if not rate limited
24
+ # end
25
+ #
26
+ class RateLimiter
27
+ attr_reader :key, :limit, :window, :remaining, :reset_at
28
+
29
+ def initialize(key:, limit:, window:, store: nil)
30
+ @key = key
31
+ @limit = limit
32
+ @window = window
33
+ @store = store || default_store
34
+ @remaining = limit
35
+ @reset_at = Time.now + window
36
+ end
37
+
38
+ # Check if request is allowed (doesn't consume a token)
39
+ def allowed?
40
+ count, = get_current_count
41
+ count < @limit
42
+ end
43
+
44
+ # Check and consume a token
45
+ def allow?
46
+ count, reset = increment
47
+ @remaining = [@limit - count, 0].max
48
+ @reset_at = reset
49
+
50
+ allowed = count <= @limit
51
+
52
+ # Track metrics
53
+ track_attempt(allowed)
54
+
55
+ allowed
56
+ end
57
+
58
+ # Alias for allow?
59
+ def throttle?
60
+ !allow?
61
+ end
62
+
63
+ # Get current usage info
64
+ def status
65
+ count, reset = get_current_count
66
+ {
67
+ key: @key,
68
+ limit: @limit,
69
+ remaining: [@limit - count, 0].max,
70
+ reset_at: reset,
71
+ used: count
72
+ }
73
+ end
74
+
75
+ # Reset the rate limit for this key
76
+ def reset!
77
+ @store.delete(@key)
78
+ @remaining = @limit
79
+ @reset_at = Time.now + @window
80
+ end
81
+
82
+ # Class method for quick throttling
83
+ def self.throttle(key, limit:, window:, store: nil)
84
+ limiter = new(key: key, limit: limit, window: window, store: store)
85
+
86
+ if limiter.allow?
87
+ yield if block_given?
88
+ true
89
+ else
90
+ false
91
+ end
92
+ end
93
+
94
+ # Check rate limit without incrementing
95
+ def self.allowed?(key, limit:, window:, store: nil)
96
+ limiter = new(key: key, limit: limit, window: window, store: store)
97
+ limiter.allowed?
98
+ end
99
+
100
+ private
101
+
102
+ def default_store
103
+ @default_store ||= MemoryStore.new
104
+ end
105
+
106
+ def get_current_count
107
+ current_bucket
108
+ data = @store.get(@key) || { buckets: {}, created_at: Time.now.to_i }
109
+
110
+ # Clean old buckets
111
+ cutoff = Time.now.to_i - @window
112
+ data[:buckets].delete_if { |k, _| k.to_i < cutoff }
113
+
114
+ count = data[:buckets].values.sum
115
+ reset = Time.at(Time.now.to_i + @window - (Time.now.to_i % @window))
116
+
117
+ [count, reset]
118
+ end
119
+
120
+ def increment
121
+ bucket = current_bucket
122
+ data = @store.get(@key) || { buckets: {}, created_at: Time.now.to_i }
123
+
124
+ # Clean old buckets
125
+ cutoff = Time.now.to_i - @window
126
+ data[:buckets].delete_if { |k, _| k.to_i < cutoff }
127
+
128
+ # Increment current bucket
129
+ data[:buckets][bucket] ||= 0
130
+ data[:buckets][bucket] += 1
131
+
132
+ # Store with TTL
133
+ @store.set(@key, data, ttl: @window * 2)
134
+
135
+ count = data[:buckets].values.sum
136
+ reset = Time.at(Time.now.to_i + @window - (Time.now.to_i % @window))
137
+
138
+ [count, reset]
139
+ end
140
+
141
+ def current_bucket
142
+ # Use 1-second buckets for sliding window
143
+ Time.now.to_i.to_s
144
+ end
145
+
146
+ def track_attempt(allowed)
147
+ return unless BrainzLab.configuration.flux_effectively_enabled?
148
+
149
+ if allowed
150
+ BrainzLab::Flux.increment('rate_limiter.allowed', tags: { key: sanitize_key(@key) })
151
+ else
152
+ BrainzLab::Flux.increment('rate_limiter.denied', tags: { key: sanitize_key(@key) })
153
+ end
154
+ end
155
+
156
+ def sanitize_key(key)
157
+ # Remove user-specific identifiers for aggregation
158
+ key.gsub(/:\d+/, ':*').gsub(/:[a-f0-9-]{36}/, ':*')
159
+ end
160
+
161
+ # Simple in-memory store (for development/single-instance)
162
+ class MemoryStore
163
+ def initialize
164
+ @data = {}
165
+ @mutex = Mutex.new
166
+ end
167
+
168
+ def get(key)
169
+ @mutex.synchronize do
170
+ entry = @data[key]
171
+ return nil unless entry
172
+ return nil if entry[:expires_at] && Time.now > entry[:expires_at]
173
+
174
+ entry[:value]
175
+ end
176
+ end
177
+
178
+ def set(key, value, ttl: nil)
179
+ @mutex.synchronize do
180
+ @data[key] = {
181
+ value: value,
182
+ expires_at: ttl ? Time.now + ttl : nil
183
+ }
184
+ end
185
+ end
186
+
187
+ def delete(key)
188
+ @mutex.synchronize do
189
+ @data.delete(key)
190
+ end
191
+ end
192
+
193
+ def clear!
194
+ @mutex.synchronize do
195
+ @data.clear
196
+ end
197
+ end
198
+ end
199
+
200
+ # Redis store adapter
201
+ class RedisStore
202
+ def initialize(redis)
203
+ @redis = redis
204
+ end
205
+
206
+ def get(key)
207
+ data = @redis.get("brainzlab:ratelimit:#{key}")
208
+ return nil unless data
209
+
210
+ JSON.parse(data, symbolize_names: true)
211
+ rescue StandardError
212
+ nil
213
+ end
214
+
215
+ def set(key, value, ttl: nil)
216
+ full_key = "brainzlab:ratelimit:#{key}"
217
+ if ttl
218
+ @redis.setex(full_key, ttl, value.to_json)
219
+ else
220
+ @redis.set(full_key, value.to_json)
221
+ end
222
+ end
223
+
224
+ def delete(key)
225
+ @redis.del("brainzlab:ratelimit:#{key}")
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'utilities/rate_limiter'
4
+ require_relative 'utilities/circuit_breaker'
5
+ require_relative 'utilities/health_check'
6
+ require_relative 'utilities/log_formatter'
7
+
8
+ module BrainzLab
9
+ module Utilities
10
+ # All utilities are auto-loaded from their respective files
11
+ # Access them via:
12
+ # BrainzLab::Utilities::RateLimiter
13
+ # BrainzLab::Utilities::CircuitBreaker
14
+ # BrainzLab::Utilities::HealthCheck
15
+ # BrainzLab::Utilities::LogFormatter
16
+ end
17
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Vault
5
+ class Cache
6
+ def initialize(ttl = 300)
7
+ @ttl = ttl
8
+ @store = {}
9
+ @mutex = Mutex.new
10
+ end
11
+
12
+ def get(key)
13
+ @mutex.synchronize do
14
+ entry = @store[key]
15
+ return nil unless entry
16
+ return nil if expired?(entry)
17
+
18
+ entry[:value]
19
+ end
20
+ end
21
+
22
+ def set(key, value)
23
+ @mutex.synchronize do
24
+ @store[key] = {
25
+ value: value,
26
+ expires_at: Time.now + @ttl
27
+ }
28
+ end
29
+ value
30
+ end
31
+
32
+ def has?(key)
33
+ @mutex.synchronize do
34
+ entry = @store[key]
35
+ return false unless entry
36
+ return false if expired?(entry)
37
+
38
+ true
39
+ end
40
+ end
41
+
42
+ def delete(key)
43
+ @mutex.synchronize do
44
+ @store.delete(key)
45
+ end
46
+ end
47
+
48
+ def delete_pattern(pattern)
49
+ @mutex.synchronize do
50
+ regex = Regexp.new(pattern.gsub('*', '.*'))
51
+ @store.delete_if { |k, _| k.match?(regex) }
52
+ end
53
+ end
54
+
55
+ def clear!
56
+ @mutex.synchronize do
57
+ @store.clear
58
+ end
59
+ end
60
+
61
+ def size
62
+ @mutex.synchronize do
63
+ cleanup_expired!
64
+ @store.size
65
+ end
66
+ end
67
+
68
+ private
69
+
70
+ def expired?(entry)
71
+ entry[:expires_at] < Time.now
72
+ end
73
+
74
+ def cleanup_expired!
75
+ now = Time.now
76
+ @store.delete_if { |_, entry| entry[:expires_at] < now }
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+
7
+ module BrainzLab
8
+ module Vault
9
+ class Client
10
+ def initialize(config)
11
+ @config = config
12
+ @base_url = config.vault_url || 'https://vault.brainzlab.ai'
13
+ end
14
+
15
+ def get(key, environment:)
16
+ response = request(
17
+ :get,
18
+ "/api/v1/secrets/#{CGI.escape(key)}",
19
+ headers: { 'X-Vault-Environment' => environment }
20
+ )
21
+
22
+ return nil unless response.is_a?(Net::HTTPSuccess)
23
+
24
+ data = JSON.parse(response.body, symbolize_names: true)
25
+ data[:value]
26
+ rescue StandardError => e
27
+ log_error('get', e)
28
+ nil
29
+ end
30
+
31
+ def set(key, value, environment:, description: nil, note: nil)
32
+ body = {
33
+ key: key,
34
+ value: value,
35
+ description: description,
36
+ note: note
37
+ }.compact
38
+
39
+ response = request(
40
+ :post,
41
+ '/api/v1/secrets',
42
+ headers: { 'X-Vault-Environment' => environment },
43
+ body: body
44
+ )
45
+
46
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
47
+ rescue StandardError => e
48
+ log_error('set', e)
49
+ false
50
+ end
51
+
52
+ def list(environment:)
53
+ response = request(
54
+ :get,
55
+ '/api/v1/secrets',
56
+ headers: { 'X-Vault-Environment' => environment }
57
+ )
58
+
59
+ return [] unless response.is_a?(Net::HTTPSuccess)
60
+
61
+ data = JSON.parse(response.body, symbolize_names: true)
62
+ data[:secrets] || []
63
+ rescue StandardError => e
64
+ log_error('list', e)
65
+ []
66
+ end
67
+
68
+ def delete(key)
69
+ response = request(:delete, "/api/v1/secrets/#{CGI.escape(key)}")
70
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPNoContent)
71
+ rescue StandardError => e
72
+ log_error('delete', e)
73
+ false
74
+ end
75
+
76
+ def export(environment:, format:)
77
+ params = { format: format }
78
+ response = request(
79
+ :get,
80
+ '/api/v1/sync/export',
81
+ headers: { 'X-Vault-Environment' => environment },
82
+ params: params
83
+ )
84
+
85
+ return {} unless response.is_a?(Net::HTTPSuccess)
86
+
87
+ case format
88
+ when :json
89
+ data = JSON.parse(response.body, symbolize_names: true)
90
+ data[:secrets] || {}
91
+ else
92
+ response.body
93
+ end
94
+ rescue StandardError => e
95
+ log_error('export', e)
96
+ format == :json ? {} : ''
97
+ end
98
+
99
+ def provision(project_id:, app_name:)
100
+ response = request(
101
+ :post,
102
+ '/api/v1/projects/provision',
103
+ body: { project_id: project_id, app_name: app_name },
104
+ use_service_key: true
105
+ )
106
+
107
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
108
+ rescue StandardError => e
109
+ log_error('provision', e)
110
+ false
111
+ end
112
+
113
+ # Get all provider keys for the current project
114
+ # Returns a hash of provider => decrypted_key
115
+ def get_provider_keys
116
+ response = request(:get, '/api/v1/provider_keys/bulk')
117
+
118
+ return {} unless response.is_a?(Net::HTTPSuccess)
119
+
120
+ data = JSON.parse(response.body, symbolize_names: true)
121
+ # Convert to simple hash: { openai: "sk-...", anthropic: "sk-..." }
122
+ keys = {}
123
+ (data[:keys] || []).each do |key_data|
124
+ keys[key_data[:provider].to_sym] = key_data[:key]
125
+ end
126
+ keys
127
+ rescue StandardError => e
128
+ log_error('get_provider_keys', e)
129
+ {}
130
+ end
131
+
132
+ # Get a specific provider key
133
+ def get_provider_key(provider:, model_type: 'llm')
134
+ response = request(
135
+ :get,
136
+ '/api/v1/provider_keys/resolve',
137
+ params: { provider: provider, model_type: model_type }
138
+ )
139
+
140
+ return nil unless response.is_a?(Net::HTTPSuccess)
141
+
142
+ data = JSON.parse(response.body, symbolize_names: true)
143
+ data[:key]
144
+ rescue StandardError => e
145
+ log_error('get_provider_key', e)
146
+ nil
147
+ end
148
+
149
+ private
150
+
151
+ def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
152
+ uri = URI.parse("#{@base_url}#{path}")
153
+
154
+ uri.query = URI.encode_www_form(params) if params
155
+
156
+ http = Net::HTTP.new(uri.host, uri.port)
157
+ http.use_ssl = uri.scheme == 'https'
158
+ http.open_timeout = 10
159
+ http.read_timeout = 30
160
+
161
+ request = case method
162
+ when :get
163
+ Net::HTTP::Get.new(uri)
164
+ when :post
165
+ Net::HTTP::Post.new(uri)
166
+ when :put
167
+ Net::HTTP::Put.new(uri)
168
+ when :delete
169
+ Net::HTTP::Delete.new(uri)
170
+ end
171
+
172
+ # Set headers
173
+ request['Content-Type'] = 'application/json'
174
+ request['Accept'] = 'application/json'
175
+
176
+ if use_service_key
177
+ request['X-Service-Key'] = @config.vault_master_key || @config.secret_key
178
+ else
179
+ auth_key = @config.vault_api_key || @config.secret_key
180
+ request['Authorization'] = "Bearer #{auth_key}" if auth_key
181
+ end
182
+
183
+ headers.each { |k, v| request[k] = v }
184
+
185
+ # Set body
186
+ request.body = body.to_json if body
187
+
188
+ http.request(request)
189
+ end
190
+
191
+ def log_error(operation, error)
192
+ structured_error = ErrorHandler.wrap(error, service: 'Vault', operation: operation)
193
+ BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{structured_error.message}")
194
+
195
+ # Call on_error callback if configured
196
+ if @config.on_error
197
+ @config.on_error.call(structured_error, { service: 'Vault', operation: operation })
198
+ end
199
+ end
200
+
201
+ def handle_response_error(response, operation)
202
+ return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
203
+
204
+ structured_error = ErrorHandler.from_response(response, service: 'Vault', operation: operation)
205
+ BrainzLab.debug_log("[Vault::Client] #{operation} failed: #{structured_error.message}")
206
+
207
+ # Call on_error callback if configured
208
+ if @config.on_error
209
+ @config.on_error.call(structured_error, { service: 'Vault', operation: operation })
210
+ end
211
+
212
+ structured_error
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Vault
5
+ class Provisioner
6
+ def initialize(config)
7
+ @config = config
8
+ @provisioned = false
9
+ end
10
+
11
+ def ensure_project!
12
+ return if @provisioned
13
+ return unless @config.vault_auto_provision
14
+ return unless valid_auth?
15
+
16
+ @provisioned = true
17
+
18
+ # Try to provision with Platform project ID
19
+ project_id = detect_project_id
20
+ return unless project_id
21
+
22
+ client = Client.new(@config)
23
+ client.provision(
24
+ project_id: project_id,
25
+ app_name: @config.app_name || @config.service
26
+ )
27
+
28
+ BrainzLab.debug_log("[Vault::Provisioner] Project provisioned: #{project_id}")
29
+ rescue StandardError => e
30
+ BrainzLab.debug_log("[Vault::Provisioner] Provisioning failed: #{e.message}")
31
+ end
32
+
33
+ private
34
+
35
+ def valid_auth?
36
+ key = @config.vault_api_key || @config.vault_master_key || @config.secret_key
37
+ !key.nil? && !key.empty?
38
+ end
39
+
40
+ def detect_project_id
41
+ # Try environment variable first
42
+ return ENV['BRAINZLAB_PROJECT_ID'] if ENV['BRAINZLAB_PROJECT_ID']
43
+
44
+ # Could also detect from Platform API if we have a secret key
45
+ nil
46
+ end
47
+ end
48
+ end
49
+ end