rails_ai 0.1.0

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 (82) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec_status +96 -0
  3. data/AGENT_GUIDE.md +513 -0
  4. data/Appraisals +49 -0
  5. data/COMMERCIAL_LICENSE_TEMPLATE.md +92 -0
  6. data/FEATURES.md +204 -0
  7. data/LEGAL_PROTECTION_GUIDE.md +222 -0
  8. data/LICENSE +62 -0
  9. data/LICENSE_SUMMARY.md +74 -0
  10. data/MIT-LICENSE +62 -0
  11. data/PERFORMANCE.md +300 -0
  12. data/PROVIDERS.md +495 -0
  13. data/README.md +454 -0
  14. data/Rakefile +11 -0
  15. data/SPEED_OPTIMIZATIONS.md +217 -0
  16. data/STRUCTURE.md +139 -0
  17. data/USAGE_GUIDE.md +288 -0
  18. data/app/channels/ai_stream_channel.rb +33 -0
  19. data/app/components/ai/prompt_component.rb +25 -0
  20. data/app/controllers/concerns/ai/context_aware.rb +77 -0
  21. data/app/controllers/concerns/ai/streaming.rb +41 -0
  22. data/app/helpers/ai_helper.rb +164 -0
  23. data/app/jobs/ai/generate_embedding_job.rb +25 -0
  24. data/app/jobs/ai/generate_summary_job.rb +25 -0
  25. data/app/models/concerns/ai/embeddable.rb +38 -0
  26. data/app/views/rails_ai/dashboard/index.html.erb +51 -0
  27. data/config/routes.rb +19 -0
  28. data/lib/generators/rails_ai/install/install_generator.rb +38 -0
  29. data/lib/rails_ai/agents/agent_manager.rb +258 -0
  30. data/lib/rails_ai/agents/agent_team.rb +243 -0
  31. data/lib/rails_ai/agents/base_agent.rb +331 -0
  32. data/lib/rails_ai/agents/collaboration.rb +238 -0
  33. data/lib/rails_ai/agents/memory.rb +116 -0
  34. data/lib/rails_ai/agents/message_bus.rb +95 -0
  35. data/lib/rails_ai/agents/specialized_agents.rb +391 -0
  36. data/lib/rails_ai/agents/task_queue.rb +111 -0
  37. data/lib/rails_ai/cache.rb +14 -0
  38. data/lib/rails_ai/config.rb +40 -0
  39. data/lib/rails_ai/context.rb +7 -0
  40. data/lib/rails_ai/context_analyzer.rb +86 -0
  41. data/lib/rails_ai/engine.rb +48 -0
  42. data/lib/rails_ai/events.rb +9 -0
  43. data/lib/rails_ai/image_context.rb +110 -0
  44. data/lib/rails_ai/performance.rb +231 -0
  45. data/lib/rails_ai/provider.rb +8 -0
  46. data/lib/rails_ai/providers/anthropic_adapter.rb +256 -0
  47. data/lib/rails_ai/providers/base.rb +60 -0
  48. data/lib/rails_ai/providers/dummy_adapter.rb +29 -0
  49. data/lib/rails_ai/providers/gemini_adapter.rb +509 -0
  50. data/lib/rails_ai/providers/openai_adapter.rb +535 -0
  51. data/lib/rails_ai/providers/secure_anthropic_adapter.rb +206 -0
  52. data/lib/rails_ai/providers/secure_openai_adapter.rb +284 -0
  53. data/lib/rails_ai/railtie.rb +48 -0
  54. data/lib/rails_ai/redactor.rb +12 -0
  55. data/lib/rails_ai/security/api_key_manager.rb +82 -0
  56. data/lib/rails_ai/security/audit_logger.rb +46 -0
  57. data/lib/rails_ai/security/error_handler.rb +62 -0
  58. data/lib/rails_ai/security/input_validator.rb +176 -0
  59. data/lib/rails_ai/security/secure_file_handler.rb +45 -0
  60. data/lib/rails_ai/security/secure_http_client.rb +177 -0
  61. data/lib/rails_ai/security.rb +0 -0
  62. data/lib/rails_ai/version.rb +5 -0
  63. data/lib/rails_ai/window_context.rb +103 -0
  64. data/lib/rails_ai.rb +502 -0
  65. data/monitoring/ci_setup_guide.md +214 -0
  66. data/monitoring/enhanced_monitoring_script.rb +237 -0
  67. data/monitoring/google_alerts_setup.md +42 -0
  68. data/monitoring_log_20250921.txt +0 -0
  69. data/monitoring_script.rb +161 -0
  70. data/rails_ai.gemspec +54 -0
  71. data/scripts/security_scanner.rb +353 -0
  72. data/setup_monitoring.sh +163 -0
  73. data/wiki/API-Documentation.md +734 -0
  74. data/wiki/Architecture-Overview.md +672 -0
  75. data/wiki/Contributing-Guide.md +407 -0
  76. data/wiki/Development-Setup.md +532 -0
  77. data/wiki/Home.md +278 -0
  78. data/wiki/Installation-Guide.md +527 -0
  79. data/wiki/Quick-Start.md +186 -0
  80. data/wiki/README.md +135 -0
  81. data/wiki/Release-Process.md +467 -0
  82. metadata +385 -0
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require_relative "../security"
6
+
7
+ module RailsAi
8
+ module Providers
9
+ class SecureAnthropicAdapter < Base
10
+ ANTHROPIC_API_BASE = "https://api.anthropic.com/v1"
11
+
12
+ def initialize
13
+ @api_key = Security::APIKeyManager.secure_fetch("ANTHROPIC_API_KEY")
14
+ @rate_limiter = Security::RateLimiter.new(limit: 50, window: 3600)
15
+ super
16
+ end
17
+
18
+ def chat!(messages:, model:, **opts)
19
+ return "(stubbed) #{messages.last[:content]}" if RailsAi.config.stub_responses
20
+
21
+ # Security validations
22
+ Security::InputValidator.validate_messages(messages)
23
+
24
+ # Rate limiting
25
+ @rate_limiter.check_limit("chat_#{RailsAi::Context.user_id}")
26
+
27
+ # Sanitize content
28
+ sanitized_messages = messages.map do |message|
29
+ {
30
+ role: message[:role],
31
+ content: Security::ContentSanitizer.sanitize_content(message[:content])
32
+ }
33
+ end
34
+
35
+ response = make_request(
36
+ "messages",
37
+ {
38
+ model: model,
39
+ max_tokens: opts[:max_tokens] || RailsAi.config.token_limit,
40
+ messages: sanitized_messages,
41
+ **opts.except(:max_tokens)
42
+ }
43
+ )
44
+
45
+ response.dig("content", 0, "text")
46
+ end
47
+
48
+ def stream_chat!(messages:, model:, **opts, &on_token)
49
+ return on_token.call("(stubbed stream)") if RailsAi.config.stub_responses
50
+
51
+ # Security validations
52
+ Security::InputValidator.validate_messages(messages)
53
+
54
+ # Rate limiting
55
+ @rate_limiter.check_limit("stream_#{RailsAi::Context.user_id}")
56
+
57
+ # Sanitize content
58
+ sanitized_messages = messages.map do |message|
59
+ {
60
+ role: message[:role],
61
+ content: Security::ContentSanitizer.sanitize_content(message[:content])
62
+ }
63
+ end
64
+
65
+ make_streaming_request(
66
+ "messages",
67
+ {
68
+ model: model,
69
+ max_tokens: opts[:max_tokens] || RailsAi.config.token_limit,
70
+ messages: sanitized_messages,
71
+ stream: true,
72
+ **opts.except(:max_tokens, :stream)
73
+ },
74
+ &on_token
75
+ )
76
+ end
77
+
78
+ def analyze_image!(image:, prompt:, model: "claude-3-5-sonnet-20241022", **opts)
79
+ return "(stubbed analysis)" if RailsAi.config.stub_responses
80
+
81
+ # Security validations
82
+ Security::InputValidator.validate_text_input(prompt, max_length: 1000)
83
+
84
+ # Rate limiting
85
+ @rate_limiter.check_limit("vision_#{RailsAi::Context.user_id}")
86
+
87
+ # Sanitize prompt
88
+ sanitized_prompt = Security::ContentSanitizer.sanitize_content(prompt)
89
+
90
+ # Prepare image securely
91
+ image_data = prepare_image_securely(image)
92
+
93
+ response = make_request(
94
+ "messages",
95
+ {
96
+ model: model,
97
+ max_tokens: opts[:max_tokens] || 1000,
98
+ messages: [
99
+ {
100
+ role: "user",
101
+ content: [
102
+ {
103
+ type: "text",
104
+ text: sanitized_prompt
105
+ },
106
+ {
107
+ type: "image",
108
+ source: {
109
+ type: "base64",
110
+ media_type: "image/png",
111
+ data: image_data
112
+ }
113
+ }
114
+ ]
115
+ }
116
+ ],
117
+ **opts.except(:max_tokens)
118
+ }
119
+ )
120
+
121
+ response.dig("content", 0, "text")
122
+ end
123
+
124
+ private
125
+
126
+ def make_request(endpoint, payload)
127
+ Security::SecureHTTPClient.make_request(
128
+ "#{ANTHROPIC_API_BASE}/#{endpoint}",
129
+ method: 'POST',
130
+ body: payload.to_json,
131
+ headers: {
132
+ 'Authorization' => "Bearer #{@api_key}",
133
+ 'Content-Type' => 'application/json',
134
+ 'Anthropic-Version' => '2023-06-01'
135
+ }
136
+ )
137
+ end
138
+
139
+ def make_streaming_request(endpoint, payload, &on_token)
140
+ Security::SecureHTTPClient.make_streaming_request(
141
+ "#{ANTHROPIC_API_BASE}/#{endpoint}",
142
+ method: 'POST',
143
+ body: payload.to_json,
144
+ headers: {
145
+ 'Authorization' => "Bearer #{@api_key}",
146
+ 'Content-Type' => 'application/json',
147
+ 'Anthropic-Version' => '2023-06-01'
148
+ }
149
+ ) do |chunk|
150
+ parse_streaming_chunk(chunk, &on_token)
151
+ end
152
+ end
153
+
154
+ def parse_streaming_chunk(chunk, &on_token)
155
+ return if chunk.strip.empty?
156
+
157
+ chunk.split("\n").each do |line|
158
+ next unless line.start_with?("data: ")
159
+
160
+ data = line[6..-1]
161
+ next if data == "[DONE]"
162
+
163
+ begin
164
+ parsed = JSON.parse(data)
165
+ content = parsed.dig("delta", "text")
166
+ on_token.call(content) if content
167
+ rescue JSON::ParserError
168
+ # Skip invalid JSON
169
+ end
170
+ end
171
+ end
172
+
173
+ def prepare_image_securely(image)
174
+ if image.is_a?(String)
175
+ if image.start_with?("data:image/")
176
+ # Validate base64 data
177
+ Security::InputValidator.validate_base64_data(image, expected_type: :image)
178
+ # Extract base64 data
179
+ image.split(",")[1]
180
+ elsif image.start_with?("http")
181
+ # Validate URL
182
+ Security::InputValidator.validate_url(image)
183
+ convert_url_to_base64_securely(image)
184
+ else
185
+ # Validate file path
186
+ Security::InputValidator.validate_file_path(image)
187
+ convert_file_to_base64_securely(image)
188
+ end
189
+ else
190
+ # File object - convert to base64
191
+ convert_file_to_base64_securely(image)
192
+ end
193
+ end
194
+
195
+ def convert_url_to_base64_securely(url)
196
+ response = Security::SecureHTTPClient.make_request(url, method: 'GET')
197
+ Base64.strict_encode64(response.body)
198
+ end
199
+
200
+ def convert_file_to_base64_securely(file_path)
201
+ file_content = Security::SecureFileHandler.safe_file_read(file_path)
202
+ Base64.strict_encode64(file_content)
203
+ end
204
+ end
205
+ end
206
+ end
@@ -0,0 +1,284 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "base64"
6
+ require_relative "../security"
7
+
8
+ module RailsAi
9
+ module Providers
10
+ class SecureOpenAIAdapter < Base
11
+ OPENAI_API_BASE = "https://api.openai.com/v1"
12
+
13
+ def initialize
14
+ @api_key = ENV.fetch("OPENAI_API_KEY")
15
+ @rate_limiter = Security::RateLimiter.new(limit: 100, window: 3600)
16
+ super
17
+ end
18
+
19
+ def chat!(messages:, model:, **opts)
20
+ return "(stubbed) #{messages.last[:content]}" if RailsAi.config.stub_responses
21
+
22
+ # Security validations
23
+ Security::InputValidator.validate_messages(messages)
24
+
25
+ # Rate limiting
26
+ @rate_limiter.check_limit("chat_#{RailsAi::Context.user_id}")
27
+
28
+ # Sanitize content
29
+ sanitized_messages = messages.map do |message|
30
+ {
31
+ role: message[:role],
32
+ content: Security::ContentSanitizer.sanitize_content(message[:content])
33
+ }
34
+ end
35
+
36
+ response = make_request(
37
+ "chat/completions",
38
+ {
39
+ model: model,
40
+ messages: sanitized_messages,
41
+ max_tokens: opts[:max_tokens] || RailsAi.config.token_limit,
42
+ temperature: opts[:temperature] || 0.7,
43
+ top_p: opts[:top_p] || 1.0,
44
+ frequency_penalty: opts[:frequency_penalty] || 0.0,
45
+ presence_penalty: opts[:presence_penalty] || 0.0,
46
+ **opts.except(:max_tokens, :temperature, :top_p, :frequency_penalty, :presence_penalty)
47
+ }
48
+ )
49
+
50
+ response.dig("choices", 0, "message", "content")
51
+ end
52
+
53
+ def stream_chat!(messages:, model:, **opts, &on_token)
54
+ return on_token.call("(stubbed stream)") if RailsAi.config.stub_responses
55
+
56
+ # Security validations
57
+ Security::InputValidator.validate_messages(messages)
58
+
59
+ # Rate limiting
60
+ @rate_limiter.check_limit("stream_#{RailsAi::Context.user_id}")
61
+
62
+ # Sanitize content
63
+ sanitized_messages = messages.map do |message|
64
+ {
65
+ role: message[:role],
66
+ content: Security::ContentSanitizer.sanitize_content(message[:content])
67
+ }
68
+ end
69
+
70
+ make_streaming_request(
71
+ "chat/completions",
72
+ {
73
+ model: model,
74
+ messages: sanitized_messages,
75
+ max_tokens: opts[:max_tokens] || RailsAi.config.token_limit,
76
+ temperature: opts[:temperature] || 0.7,
77
+ top_p: opts[:top_p] || 1.0,
78
+ frequency_penalty: opts[:frequency_penalty] || 0.0,
79
+ presence_penalty: opts[:presence_penalty] || 0.0,
80
+ stream: true,
81
+ **opts.except(:max_tokens, :temperature, :top_p, :frequency_penalty, :presence_penalty, :stream)
82
+ },
83
+ &on_token
84
+ )
85
+ end
86
+
87
+ def generate_image!(prompt:, model: "dall-e-3", size: "1024x1024", quality: "standard", **opts)
88
+ return "(stubbed image)" if RailsAi.config.stub_responses
89
+
90
+ # Security validations
91
+ Security::InputValidator.validate_text_input(prompt, max_length: 1000)
92
+
93
+ # Rate limiting
94
+ @rate_limiter.check_limit("image_#{RailsAi::Context.user_id}")
95
+
96
+ # Sanitize prompt
97
+ sanitized_prompt = Security::ContentSanitizer.sanitize_content(prompt)
98
+
99
+ response = make_request(
100
+ "images/generations",
101
+ {
102
+ model: model,
103
+ prompt: sanitized_prompt,
104
+ size: size,
105
+ quality: quality,
106
+ n: opts[:n] || 1,
107
+ **opts.except(:n)
108
+ }
109
+ )
110
+
111
+ response.dig("data", 0, "url")
112
+ end
113
+
114
+ def analyze_image!(image:, prompt:, model: "gpt-4o", **opts)
115
+ return "(stubbed analysis)" if RailsAi.config.stub_responses
116
+
117
+ # Security validations
118
+ Security::InputValidator.validate_text_input(prompt, max_length: 1000)
119
+
120
+ # Rate limiting
121
+ @rate_limiter.check_limit("vision_#{RailsAi::Context.user_id}")
122
+
123
+ # Sanitize prompt
124
+ sanitized_prompt = Security::ContentSanitizer.sanitize_content(prompt)
125
+
126
+ # Prepare image securely
127
+ image_data = prepare_image_securely(image)
128
+
129
+ response = make_request(
130
+ "chat/completions",
131
+ {
132
+ model: model,
133
+ messages: [
134
+ {
135
+ role: "user",
136
+ content: [
137
+ {
138
+ type: "text",
139
+ text: sanitized_prompt
140
+ },
141
+ {
142
+ type: "image_url",
143
+ image_url: {
144
+ url: image_data
145
+ }
146
+ }
147
+ ]
148
+ }
149
+ ],
150
+ max_tokens: opts[:max_tokens] || 1000,
151
+ **opts.except(:max_tokens)
152
+ }
153
+ )
154
+
155
+ response.dig("choices", 0, "message", "content")
156
+ end
157
+
158
+ private
159
+
160
+ def make_request(endpoint, payload)
161
+ uri = URI("#{OPENAI_API_BASE}/#{endpoint}")
162
+
163
+ http = Net::HTTP.new(uri.host, uri.port)
164
+ http.use_ssl = true
165
+ http.read_timeout = 30
166
+ http.open_timeout = 10
167
+
168
+ request = Net::HTTP::Post.new(uri)
169
+ request["Authorization"] = "Bearer #{@api_key}"
170
+ request["Content-Type"] = "application/json"
171
+ request.body = payload.to_json
172
+
173
+ response = http.request(request)
174
+
175
+ case response.code.to_i
176
+ when 200..299
177
+ JSON.parse(response.body)
178
+ when 429
179
+ raise Security::RateLimitError, "OpenAI API rate limit exceeded"
180
+ when 401
181
+ raise Security::SecurityError, "Invalid API key"
182
+ when 403
183
+ raise Security::SecurityError, "API access forbidden"
184
+ else
185
+ raise Security::SecurityError, "API request failed: #{response.code} #{response.message}"
186
+ end
187
+ end
188
+
189
+ def make_streaming_request(endpoint, payload, &on_token)
190
+ uri = URI("#{OPENAI_API_BASE}/#{endpoint}")
191
+
192
+ http = Net::HTTP.new(uri.host, uri.port)
193
+ http.use_ssl = true
194
+ http.read_timeout = 60
195
+ http.open_timeout = 10
196
+
197
+ request = Net::HTTP::Post.new(uri)
198
+ request["Authorization"] = "Bearer #{@api_key}"
199
+ request["Content-Type"] = "application/json"
200
+ request.body = payload.to_json
201
+
202
+ http.request(request) do |response|
203
+ case response.code.to_i
204
+ when 200..299
205
+ response.read_body do |chunk|
206
+ next if chunk.strip.empty?
207
+
208
+ chunk.split("\n").each do |line|
209
+ next unless line.start_with?("data: ")
210
+
211
+ data = line[6..-1]
212
+ next if data == "[DONE]"
213
+
214
+ begin
215
+ parsed = JSON.parse(data)
216
+ content = parsed.dig("choices", 0, "delta", "content")
217
+ on_token.call(content) if content
218
+ rescue JSON::ParserError
219
+ # Skip invalid JSON
220
+ end
221
+ end
222
+ end
223
+ when 429
224
+ raise Security::RateLimitError, "OpenAI API rate limit exceeded"
225
+ when 401
226
+ raise Security::SecurityError, "Invalid API key"
227
+ when 403
228
+ raise Security::SecurityError, "API access forbidden"
229
+ else
230
+ raise Security::SecurityError, "API request failed: #{response.code} #{response.message}"
231
+ end
232
+ end
233
+ end
234
+
235
+ def prepare_image_securely(image)
236
+ if image.is_a?(String)
237
+ if image.start_with?("data:image/")
238
+ # Validate base64 data
239
+ Security::InputValidator.validate_base64_data(image, expected_type: :image)
240
+ image
241
+ elsif image.start_with?("http")
242
+ # Validate URL
243
+ Security::InputValidator.validate_url(image)
244
+ convert_url_to_base64_securely(image)
245
+ else
246
+ # Validate file path
247
+ Security::InputValidator.validate_file_path(image)
248
+ convert_file_to_base64_securely(image)
249
+ end
250
+ else
251
+ # File object - convert to base64
252
+ convert_file_to_base64_securely(image)
253
+ end
254
+ end
255
+
256
+ def convert_url_to_base64_securely(url)
257
+ uri = URI(url)
258
+
259
+ http = Net::HTTP.new(uri.host, uri.port)
260
+ http.use_ssl = true
261
+ http.read_timeout = 30
262
+ http.open_timeout = 10
263
+
264
+ request = Net::HTTP::Get.new(uri)
265
+ request["User-Agent"] = "RailsAI/1.0"
266
+
267
+ response = http.request(request)
268
+
269
+ if response.code == "200"
270
+ base64_data = Base64.strict_encode64(response.body)
271
+ "data:image/png;base64,#{base64_data}"
272
+ else
273
+ raise Security::SecurityError, "Failed to fetch image from URL: #{response.code}"
274
+ end
275
+ end
276
+
277
+ def convert_file_to_base64_securely(file_path)
278
+ file_content = Security::SecureFileHandler.safe_file_read(file_path)
279
+ base64_data = Base64.strict_encode64(file_content)
280
+ "data:image/png;base64,#{base64_data}"
281
+ end
282
+ end
283
+ end
284
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ class Railtie < ::Rails::Railtie
5
+ config.rails_ai = ActiveSupport::OrderedOptions.new
6
+
7
+ # Rails version compatibility
8
+ if Rails.version >= "8.0"
9
+ # Rails 8 specific configuration
10
+ initializer "rails_ai.configure", before: :load_config_initializers do |app|
11
+ RailsAi.configure do |c|
12
+ c.provider ||= app.config.rails_ai.provider || :openai
13
+ c.default_model ||= app.config.rails_ai.default_model || "gpt-4o-mini"
14
+ c.token_limit ||= app.config.rails_ai.token_limit || 4000
15
+ c.cache_ttl ||= app.config.rails_ai.cache_ttl || 1.hour
16
+ c.stub_responses ||= app.config.rails_ai.stub_responses || false
17
+ end
18
+ end
19
+ elsif Rails.version >= "7.0"
20
+ # Rails 7 specific configuration
21
+ initializer "rails_ai.configure", before: :load_config_initializers do |app|
22
+ RailsAi.configure do |c|
23
+ c.provider ||= app.config.rails_ai.provider || :openai
24
+ c.default_model ||= app.config.rails_ai.default_model || "gpt-4o-mini"
25
+ c.token_limit ||= app.config.rails_ai.token_limit || 4000
26
+ c.cache_ttl ||= app.config.rails_ai.cache_ttl || 1.hour
27
+ c.stub_responses ||= app.config.rails_ai.stub_responses || false
28
+ end
29
+ end
30
+ else
31
+ # Rails 5.2+ and 6.x configuration
32
+ initializer "rails_ai.configure" do |app|
33
+ RailsAi.configure do |c|
34
+ c.provider ||= app.config.rails_ai.provider || :openai
35
+ c.default_model ||= app.config.rails_ai.default_model || "gpt-4o-mini"
36
+ c.token_limit ||= app.config.rails_ai.token_limit || 4000
37
+ c.cache_ttl ||= app.config.rails_ai.cache_ttl || 1.hour
38
+ c.stub_responses ||= app.config.rails_ai.stub_responses || false
39
+ end
40
+ end
41
+ end
42
+
43
+ # Common configuration for all Rails versions
44
+ initializer "rails_ai.logger" do
45
+ Rails.logger.info "Rails AI #{RailsAi::VERSION} loaded for Rails #{Rails.version}"
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ module Redactor
5
+ EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i
6
+ PHONE = /\+?\d[\d\s().-]{7,}\d/
7
+
8
+ def self.call(text)
9
+ text.to_s.gsub(EMAIL, "[email]").gsub(PHONE, "[phone]")
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+ require 'securerandom'
5
+
6
+ module RailsAi
7
+ module Security
8
+ class APIKeyManager
9
+ def self.encrypt_key(key)
10
+ return nil if key.nil? || key.empty?
11
+
12
+ cipher = OpenSSL::Cipher.new('AES-256-GCM')
13
+ cipher.encrypt
14
+ cipher.key = derive_key
15
+ cipher.iv = SecureRandom.random_bytes(12)
16
+
17
+ encrypted = cipher.update(key) + cipher.final
18
+ auth_tag = cipher.auth_tag
19
+
20
+ Base64.strict_encode64({
21
+ data: Base64.strict_encode64(encrypted),
22
+ iv: Base64.strict_encode64(cipher.iv),
23
+ auth_tag: Base64.strict_encode64(auth_tag)
24
+ }.to_json)
25
+ end
26
+
27
+ def self.decrypt_key(encrypted_key)
28
+ return nil if encrypted_key.nil? || encrypted_key.empty?
29
+
30
+ begin
31
+ key_data = JSON.parse(Base64.strict_decode64(encrypted_key))
32
+
33
+ cipher = OpenSSL::Cipher.new('AES-256-GCM')
34
+ cipher.decrypt
35
+ cipher.key = derive_key
36
+ cipher.iv = Base64.strict_decode64(key_data['iv'])
37
+ cipher.auth_tag = Base64.strict_decode64(key_data['auth_tag'])
38
+
39
+ encrypted_data = Base64.strict_decode64(key_data['data'])
40
+ cipher.update(encrypted_data) + cipher.final
41
+ rescue => e
42
+ raise SecurityError, "Failed to decrypt API key: #{e.message}"
43
+ end
44
+ end
45
+
46
+ def self.mask_key(key)
47
+ return nil if key.nil? || key.empty?
48
+ return "***" if key.length < 8
49
+
50
+ "#{key[0..3]}***#{key[-4..-1]}"
51
+ end
52
+
53
+ def self.secure_fetch(env_var, required: true)
54
+ key = ENV[env_var]
55
+
56
+ if key.nil? || key.empty?
57
+ if required
58
+ raise SecurityError, "Required environment variable #{env_var} is not set"
59
+ else
60
+ return nil
61
+ end
62
+ end
63
+
64
+ # Check if key is already encrypted
65
+ if key.start_with?('{') && key.include?('"data"')
66
+ decrypt_key(key)
67
+ else
68
+ key
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def self.derive_key
75
+ secret = ENV['RAILS_AI_SECRET'] || Rails.application&.secret_key_base || 'default_secret'
76
+ salt = ENV['RAILS_AI_SALT'] || 'rails_ai_default_salt'
77
+
78
+ OpenSSL::PKCS5.pbkdf2_hmac_sha256(secret, salt, 100000, 32)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAi
4
+ module Security
5
+ class AuditLogger
6
+ def self.log_security_event(event_type, details = {})
7
+ return unless defined?(Rails) && Rails.logger
8
+
9
+ log_entry = {
10
+ timestamp: Time.now.iso8601,
11
+ event_type: event_type,
12
+ details: details,
13
+ user_id: RailsAi::Context.user_id,
14
+ request_id: RailsAi::Context.request_id
15
+ }
16
+
17
+ Rails.logger.info("[SECURITY] #{log_entry.to_json}")
18
+ end
19
+
20
+ def self.log_api_call(endpoint, user_id, success: true)
21
+ log_security_event(:api_call, {
22
+ endpoint: endpoint,
23
+ user_id: user_id,
24
+ success: success,
25
+ timestamp: Time.now
26
+ })
27
+ end
28
+
29
+ def self.log_validation_error(error_type, details)
30
+ log_security_event(:validation_error, {
31
+ error_type: error_type,
32
+ details: details,
33
+ timestamp: Time.now
34
+ })
35
+ end
36
+
37
+ def self.log_rate_limit_exceeded(identifier, limit)
38
+ log_security_event(:rate_limit_exceeded, {
39
+ identifier: identifier,
40
+ limit: limit,
41
+ timestamp: Time.now
42
+ })
43
+ end
44
+ end
45
+ end
46
+ end