reputable 0.1.8 → 0.1.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b987ffa68c839c4a7d7a288fbe89f530eebeed3ba7f793ae7f9dc99ffa09063
4
- data.tar.gz: 4d83e36696d0b1164958dc32be975f4b18dbdc9e2d67e25e586cbdced3c690f3
3
+ metadata.gz: e8d9854d3f0dfbf85029fa9411273ffdebad9288eaa11b91649a3aec538b6b2b
4
+ data.tar.gz: 8d7fbfaf636c06c80a1ea291a773d35c1e478f5dd1261efd6e213a473f1d0362
5
5
  SHA512:
6
- metadata.gz: a89be2adeabee55f6ef8599dff51e6fbdff659de9cc7657b1d82ae61e14d61a52985533f067939500f89b19e36b844be4baf9b882d2926f29c7d899b5da6fa1a
7
- data.tar.gz: bb16577e080d26d0ba106e8dbbc777b48b030c1bdc83bae5fba75df56f06db46557a0aa92318e9e82e7f04722065e0f3b7fb28adcf80dd400b9f3810e14b84a2
6
+ metadata.gz: a13fdb389753a6aed4bde2d9e6bc2304e184db0ded7a579adc6d7fef16d4e1e72a6032f11eec7e6e03e0e706aa2a135c07c17e3e607e227f7670864e84c91aea
7
+ data.tar.gz: fb215dbd673959e3414eaa31c0a77a3c78366db796f34b1afbef12d066041b1308b0c6312a90dc7ee953a8b578083192509a535fb3bf9b53f265f9f3b9a22f80
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reputable (0.1.6)
4
+ reputable (0.1.9)
5
5
  connection_pool (~> 2.2)
6
6
  redis (>= 4.0, < 6.0)
7
7
 
data/README.md CHANGED
@@ -4,6 +4,8 @@ Ruby gem for integrating with Reputable - bot detection and reputation scoring f
4
4
 
5
5
  **Resilience First**: This gem is designed to never break your application. All operations fail silently with safe defaults.
6
6
 
7
+ Release notes and version bumps: see `clients/ruby/RELEASING.md`.
8
+
7
9
  ## Installation
8
10
 
9
11
  Add to your Gemfile:
@@ -83,6 +85,11 @@ REPUTABLE_REDIS_URL=rediss://user:password@your-dragonfly.example.com:6379
83
85
  # Optional: Base URL for verification and API endpoints (domain only)
84
86
  REPUTABLE_BASE_URL=https://api.reputable.click
85
87
 
88
+ # Optional: Blocked page branding/support info
89
+ REPUTABLE_SITE_NAME="Example Store"
90
+ REPUTABLE_SUPPORT_EMAIL=support@example.com
91
+ REPUTABLE_SUPPORT_URL=https://example.com/support
92
+
86
93
  # Optional: Disable entirely (useful for test environments)
87
94
  REPUTABLE_ENABLED=false
88
95
 
@@ -142,6 +149,11 @@ Reputable.configure do |config|
142
149
  # Supports comma-separated list in REPUTABLE_TRUSTED_KEYS or single key in REPUTABLE_TRUSTED_KEY
143
150
  config.trusted_keys = ENV['REPUTABLE_TRUSTED_KEYS']&.split(',') || ENV['REPUTABLE_TRUSTED_KEY']
144
151
  config.base_url = ENV['REPUTABLE_BASE_URL'] # Domain only
152
+
153
+ # Optional: blocked page branding/support info
154
+ config.site_name = ENV['REPUTABLE_SITE_NAME']
155
+ config.support_email = ENV['REPUTABLE_SUPPORT_EMAIL']
156
+ config.support_url = ENV['REPUTABLE_SUPPORT_URL']
145
157
 
146
158
  # Error callback (optional)
147
159
  config.on_error = ->(error, context) {
@@ -260,6 +272,27 @@ config.middleware.use Reputable::Middleware,
260
272
  async: true
261
273
  ```
262
274
 
275
+ ### Optional Reputation Gate
276
+
277
+ If you want the middleware to enforce IP reputation decisions, enable the gate:
278
+
279
+ ```ruby
280
+ config.middleware.use Reputable::Middleware,
281
+ reputation_gate: true,
282
+ challenge_action: :verify, # Redirect to verification for untrusted_challenge
283
+ block_action: :blocked_page_remote, # Redirect to hosted blocked page (uses app UI settings)
284
+ blocked_page_path: "/_reputable/blocked" # Only used for local blocked page
285
+ ```
286
+
287
+ Notes:
288
+ - For `untrusted_challenge`, the middleware redirects to the Reputable verification URL.
289
+ - For `untrusted_block`, the default is to redirect to the hosted blocked page (`/_reputable/verify/blocked`).
290
+ - The hosted blocked page uses the same app UI settings as the verify/failure pages (`siteName`, `supportEmail`).
291
+ - To render a local blocked page instead, set `block_action: :blocked_page` and pass `blocked_page` options.
292
+ - To use a custom hosted page, set `blocked_redirect_url: "https://example.com/blocked"`.
293
+ - Use `blocked_page_path` only for local blocked pages (or to build a custom `failure_url`).
294
+ - Override `challenge_redirect_status` (default `302`) or `verification_force_challenge` if needed.
295
+
263
296
  ### Default Skipped Paths
264
297
 
265
298
  The middleware automatically skips:
@@ -330,6 +363,31 @@ reputable_verified?
330
363
  clear_reputable_verification!
331
364
  ```
332
365
 
366
+ ### Reputation Gate Helpers
367
+
368
+ ```ruby
369
+ class SessionsController < ApplicationController
370
+ def new
371
+ require_reputable_reputation!
372
+ # If not blocked/challenged, continue
373
+ end
374
+ end
375
+ ```
376
+
377
+ `require_reputable_reputation!` will:
378
+ - Render a blocked page for `untrusted_block`
379
+ - Run verification flow for `untrusted_challenge`
380
+
381
+ You can also render the blocked page directly:
382
+
383
+ ```ruby
384
+ render_reputable_blocked_page(
385
+ site_name: "Example Store",
386
+ support_email: "support@example.com",
387
+ support_url: "https://example.com/support"
388
+ )
389
+ ```
390
+
333
391
  ---
334
392
 
335
393
  ## Manual API Usage
@@ -14,7 +14,8 @@ module Reputable
14
14
  :default_ttls, :pool_size, :pool_timeout,
15
15
  :connect_timeout, :read_timeout, :write_timeout,
16
16
  :ssl_params, :trusted_proxies, :ip_header_priority,
17
- :on_error, :trusted_keys, :base_url
17
+ :on_error, :trusted_keys, :base_url,
18
+ :site_name, :support_email, :support_url
18
19
 
19
20
  # Alias for backward compatibility
20
21
  alias_method :verification_base_url, :base_url
@@ -78,6 +79,9 @@ module Reputable
78
79
  @trusted_keys = [ENV["REPUTABLE_SECRET_KEY"]]
79
80
  end
80
81
  @base_url = ENV.fetch("REPUTABLE_BASE_URL", "https://api.reputable.click")
82
+ @site_name = ENV["REPUTABLE_SITE_NAME"]
83
+ @support_email = ENV["REPUTABLE_SUPPORT_EMAIL"]
84
+ @support_url = ENV["REPUTABLE_SUPPORT_URL"]
81
85
  end
82
86
 
83
87
  # Alias for backward compatibility
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "blocked_page"
4
+
3
5
  module Reputable
4
6
  # Rack middleware for automatic request tracking
5
7
  #
@@ -42,12 +44,34 @@ module Reputable
42
44
  @skip_if = options[:skip_if]
43
45
  @tag_builder = options[:tag_builder]
44
46
  @async = options.fetch(:async, true)
47
+ @reputation_gate = options.fetch(:reputation_gate, false)
48
+ @challenge_action = options.fetch(:challenge_action, :verify)
49
+ @block_action = options.fetch(:block_action, :blocked_page_remote)
50
+ @challenge_redirect_status = options.fetch(:challenge_redirect_status, 302)
51
+ @blocked_redirect_status = options.fetch(:blocked_redirect_status, 302)
52
+ @blocked_redirect_url = options[:blocked_redirect_url]
53
+ @verification_force_challenge = options.fetch(:verification_force_challenge, false)
54
+ @verification_failure_url = options[:verification_failure_url]
55
+ @verification_session_id = options[:verification_session_id]
56
+ @verified_session_keys = Array(options.fetch(:verified_session_keys, [:reputable_verified_at, :reputable_verified]))
57
+ @blocked_page_options = options.fetch(:blocked_page, {})
58
+ @blocked_page_path = options[:blocked_page_path]
45
59
  end
46
60
 
47
61
  def call(env)
48
62
  # Check for verification return parameters and verify signature if present
49
63
  handle_verification_return(env)
50
64
 
65
+ # Optional: render blocked page path directly
66
+ if (blocked_response = handle_blocked_page_request(env))
67
+ return blocked_response
68
+ end
69
+
70
+ # Optional: enforce reputation gate before app
71
+ if (gate_response = safe_reputation_gate(env))
72
+ return gate_response
73
+ end
74
+
51
75
  # ALWAYS process the request first - tracking must never block
52
76
  status, headers, response = @app.call(env)
53
77
 
@@ -60,6 +84,33 @@ module Reputable
60
84
 
61
85
  private
62
86
 
87
+ def safe_reputation_gate(env)
88
+ return nil unless @reputation_gate
89
+ return nil unless Reputable.enabled?
90
+ return nil if skip_request?(env)
91
+
92
+ enforce_reputation_gate(env)
93
+ rescue StandardError => e
94
+ Reputable.logger&.debug("Reputable reputation gate: #{e.class} - #{e.message}")
95
+ nil
96
+ end
97
+
98
+ def enforce_reputation_gate(env)
99
+ ip = extract_ip(env)
100
+ status = Reputable::Reputation.lookup_ip(ip)
101
+ return nil if status.nil?
102
+
103
+ case status
104
+ when "untrusted_block"
105
+ handle_block_action(env, ip)
106
+ when "untrusted_challenge"
107
+ return nil if verified_session?(env)
108
+ handle_challenge_action(env)
109
+ else
110
+ nil
111
+ end
112
+ end
113
+
63
114
  def safe_track_request(env)
64
115
  # Skip if disabled globally
65
116
  return unless Reputable.enabled?
@@ -93,6 +144,145 @@ module Reputable
93
144
  Reputable.logger&.debug("Reputable verification error: #{e.message}")
94
145
  end
95
146
 
147
+ def handle_blocked_page_request(env)
148
+ return nil unless @blocked_page_path
149
+
150
+ path = env["PATH_INFO"] || "/"
151
+ return nil unless path == @blocked_page_path
152
+
153
+ ip = extract_ip(env)
154
+ build_blocked_page_response(ip)
155
+ rescue StandardError => e
156
+ Reputable.logger&.debug("Reputable blocked page: #{e.class} - #{e.message}")
157
+ nil
158
+ end
159
+
160
+ def handle_block_action(env, ip)
161
+ case @block_action
162
+ when :blocked_page
163
+ build_blocked_page_response(ip)
164
+ when :blocked_page_remote
165
+ url = resolve_blocked_redirect_url(env) || Reputable.blocked_page_url
166
+ redirect_response(url, @blocked_redirect_status)
167
+ when :forbidden
168
+ [403, { "Content-Type" => "text/plain; charset=utf-8" }, ["Forbidden"]]
169
+ when Proc
170
+ @block_action.call(env, ip)
171
+ else
172
+ build_blocked_page_response(ip)
173
+ end
174
+ end
175
+
176
+ def handle_challenge_action(env)
177
+ case @challenge_action
178
+ when :verify
179
+ keys = Reputable.configuration.trusted_keys
180
+ return nil if keys.nil? || keys.empty?
181
+
182
+ request = Rack::Request.new(env)
183
+ return_url = request.url
184
+ failure_url = build_verification_failure_url(request)
185
+ session_id = resolve_session_id(env)
186
+
187
+ redirect_url = Reputable.verification_url(
188
+ return_url: return_url,
189
+ failure_url: failure_url,
190
+ session_id: session_id,
191
+ force_challenge: @verification_force_challenge
192
+ )
193
+
194
+ return nil if redirect_url == return_url
195
+
196
+ redirect_response(redirect_url, @challenge_redirect_status)
197
+ when Proc
198
+ @challenge_action.call(env)
199
+ else
200
+ nil
201
+ end
202
+ end
203
+
204
+ def build_verification_failure_url(request)
205
+ if @verification_failure_url.respond_to?(:call)
206
+ @verification_failure_url.call(request)
207
+ elsif @verification_failure_url
208
+ @verification_failure_url
209
+ elsif @blocked_page_path
210
+ base = request.base_url
211
+ path = @blocked_page_path.start_with?("/") ? @blocked_page_path : "/#{@blocked_page_path}"
212
+ "#{base}#{path}"
213
+ end
214
+ rescue StandardError
215
+ nil
216
+ end
217
+
218
+ def resolve_session_id(env)
219
+ return @verification_session_id.call(env) if @verification_session_id.respond_to?(:call)
220
+ return @verification_session_id if @verification_session_id.is_a?(String)
221
+
222
+ if @verification_session_id.is_a?(Symbol)
223
+ session = env["rack.session"]
224
+ return session[@verification_session_id] if session
225
+ end
226
+
227
+ session = env["rack.session"]
228
+ return nil unless session
229
+
230
+ return session.id if session.respond_to?(:id) && session.id
231
+
232
+ options = env["rack.session.options"]
233
+ return options[:id] if options.is_a?(Hash) && options[:id]
234
+
235
+ session[:session_id] || session["session_id"]
236
+ rescue StandardError
237
+ nil
238
+ end
239
+
240
+ def verified_session?(env)
241
+ return true if env["reputable.verified"]
242
+
243
+ session = env["rack.session"]
244
+ return false unless session
245
+
246
+ @verified_session_keys.any? do |key|
247
+ session[key] || session[key.to_s]
248
+ end
249
+ rescue StandardError
250
+ false
251
+ end
252
+
253
+ def build_blocked_page_response(client_ip)
254
+ options = blocked_page_options.merge(client_ip: client_ip)
255
+ Reputable::BlockedPage.response(**options)
256
+ end
257
+
258
+ def blocked_page_options
259
+ config = Reputable.configuration
260
+ defaults = {
261
+ site_name: config.site_name,
262
+ support_email: config.support_email,
263
+ support_url: config.support_url
264
+ }
265
+ defaults.merge(@blocked_page_options)
266
+ end
267
+
268
+ def redirect_response(url, status)
269
+ headers = {
270
+ "Location" => url,
271
+ "Content-Type" => "text/html; charset=utf-8",
272
+ "Cache-Control" => "no-store"
273
+ }
274
+ [status, headers, ["Redirecting to verification..."]]
275
+ end
276
+
277
+ def resolve_blocked_redirect_url(env)
278
+ return @blocked_redirect_url.call(env) if @blocked_redirect_url.respond_to?(:call)
279
+ return @blocked_redirect_url if @blocked_redirect_url.is_a?(String)
280
+
281
+ nil
282
+ rescue StandardError
283
+ nil
284
+ end
285
+
96
286
  def skip_request?(env)
97
287
  return true if @skip_if&.call(env)
98
288
 
@@ -90,6 +90,30 @@ module Reputable
90
90
  # Verification redirect helpers
91
91
  # ========================================
92
92
 
93
+ # Render the standard blocked page
94
+ def render_reputable_blocked_page(
95
+ status: 403,
96
+ site_name: nil,
97
+ support_email: nil,
98
+ support_url: nil,
99
+ heading: "Access blocked",
100
+ message: nil,
101
+ show_ip: true
102
+ )
103
+ config = Reputable.configuration
104
+ html = Reputable::BlockedPage.html(
105
+ site_name: site_name || config.site_name,
106
+ support_email: support_email || config.support_email,
107
+ support_url: support_url || config.support_url,
108
+ client_ip: request.remote_ip,
109
+ heading: heading,
110
+ message: message,
111
+ show_ip: show_ip
112
+ )
113
+
114
+ render html: html.html_safe, status: status, content_type: "text/html"
115
+ end
116
+
93
117
  # Check if the current session has already passed verification
94
118
  # @param session_key [Symbol]
95
119
  # @return [Boolean]
@@ -143,6 +167,47 @@ module Reputable
143
167
  ) and return
144
168
  end
145
169
 
170
+ # Enforce reputation-based access control.
171
+ # - If IP is blocked, render a blocked page.
172
+ # - If IP is challenged, run verification flow.
173
+ def require_reputable_reputation!(
174
+ return_url: request.original_url,
175
+ failure_url: nil,
176
+ session_id: session.id,
177
+ force_challenge: false,
178
+ session_key: :reputable_verified_at,
179
+ blocked_status: 403,
180
+ blocked_site_name: nil,
181
+ blocked_support_email: nil,
182
+ blocked_support_url: nil,
183
+ blocked_heading: "Access blocked",
184
+ blocked_message: nil,
185
+ blocked_show_ip: true
186
+ )
187
+ if current_ip_blocked?
188
+ render_reputable_blocked_page(
189
+ status: blocked_status,
190
+ site_name: blocked_site_name,
191
+ support_email: blocked_support_email,
192
+ support_url: blocked_support_url,
193
+ heading: blocked_heading,
194
+ message: blocked_message,
195
+ show_ip: blocked_show_ip
196
+ )
197
+ return
198
+ end
199
+
200
+ return unless current_ip_challenged?
201
+
202
+ require_reputable_verification!(
203
+ return_url: return_url,
204
+ failure_url: failure_url,
205
+ session_id: session_id,
206
+ force_challenge: force_challenge,
207
+ session_key: session_key
208
+ )
209
+ end
210
+
146
211
  private
147
212
 
148
213
  def reputable_verification_url(return_url:, failure_url:, session_id:, force_challenge:)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reputable
4
- VERSION = "0.1.8"
4
+ VERSION = "0.1.9"
5
5
  end
data/lib/reputable.rb CHANGED
@@ -7,6 +7,7 @@ require_relative "reputable/configuration"
7
7
  require_relative "reputable/connection"
8
8
  require_relative "reputable/tracker"
9
9
  require_relative "reputable/reputation"
10
+ require_relative "reputable/blocked_page"
10
11
  require_relative "reputable/middleware"
11
12
 
12
13
  # Optional Rails integration (only load if Rails is defined)
@@ -171,6 +172,14 @@ module Reputable
171
172
  "#{verify_url}?token=#{token}"
172
173
  end
173
174
 
175
+ # Build the hosted blocked page URL
176
+ # @return [String]
177
+ def blocked_page_url
178
+ base_url = configuration.base_url
179
+ base_url = base_url.chomp("/")
180
+ "#{base_url}/_reputable/verify/blocked"
181
+ end
182
+
174
183
  # Verify the signature of a redirect return
175
184
  # @param params [Hash] Request query parameters
176
185
  # @return [Boolean] true if valid logic and passed signature check
data/reputable.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/reputable/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "reputable"
7
+ spec.version = Reputable::VERSION
8
+ spec.authors = ["Reputable"]
9
+ spec.email = ["support@reputable.click"]
10
+
11
+ spec.summary = "Ruby client for Reputable - bot detection and reputation scoring"
12
+ spec.description = "Track requests and manage IP reputation through Redis/Dragonfly integration with Reputable"
13
+ spec.homepage = "https://github.com/reputable-click/reputable-rb"
14
+ spec.license = "MIT"
15
+ spec.required_ruby_version = ">= 2.7.0"
16
+
17
+ spec.metadata["homepage_uri"] = spec.homepage
18
+ spec.metadata["source_code_uri"] = spec.homepage
19
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)}) || f.end_with?(".gem")
24
+ end
25
+ end
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_dependency "redis", ">= 4.0", "< 6.0"
31
+ spec.add_dependency "connection_pool", "~> 2.2"
32
+
33
+ spec.add_development_dependency "bundler", "~> 2.0"
34
+ spec.add_development_dependency "rake", "~> 13.0"
35
+ spec.add_development_dependency "rspec", "~> 3.12"
36
+ spec.add_development_dependency "rack", "~> 2.0"
37
+ spec.add_development_dependency "rubocop", "~> 1.0"
38
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: reputable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reputable
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-12-28 00:00:00.000000000 Z
11
+ date: 2025-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -135,6 +135,7 @@ files:
135
135
  - lib/reputable/reputation.rb
136
136
  - lib/reputable/tracker.rb
137
137
  - lib/reputable/version.rb
138
+ - reputable.gemspec
138
139
  homepage: https://github.com/reputable-click/reputable-rb
139
140
  licenses:
140
141
  - MIT