reputable 0.1.8 → 0.1.10

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: 10ace4d8c54f4fe4bcf4a1a2bbba66015dbff719b353ee9331e9f3ad56400c69
4
+ data.tar.gz: d3044684df204b097e1a739f29425947306ed9c5533d4a20c66b1064dafc8892
5
5
  SHA512:
6
- metadata.gz: a89be2adeabee55f6ef8599dff51e6fbdff659de9cc7657b1d82ae61e14d61a52985533f067939500f89b19e36b844be4baf9b882d2926f29c7d899b5da6fa1a
7
- data.tar.gz: bb16577e080d26d0ba106e8dbbc777b48b030c1bdc83bae5fba75df56f06db46557a0aa92318e9e82e7f04722065e0f3b7fb28adcf80dd400b9f3810e14b84a2
6
+ metadata.gz: d3e578a48821efe6610ee60d0560c366fa2136e4fcbc93274822abda5cfb776e0aa924fe7877ff3c2164b177328fe630734fbf5d972103d741d5fc5f8db3f65b
7
+ data.tar.gz: 077b28a5af24557650e4e82759f63656532da2586f200b68a1dee2ebb6315c238efa44bf8dbb5a58634eb3cecb1819649772abeb65f78efcd064339453d842e3
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.10)
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
 
@@ -122,7 +129,7 @@ Reputable.configure do |config|
122
129
  # Customize TTLs (in seconds, 0 = forever)
123
130
  config.default_ttls = {
124
131
  trusted_verified: 0, # Forever
125
- trusted_behavior: 30 * 24 * 3600, # 30 days
132
+ trusted_behavior: 365 * 24 * 3600, # 1 year
126
133
  untrusted_challenge: 7 * 24 * 3600,
127
134
  untrusted_block: 7 * 24 * 3600,
128
135
  untrusted_ignore: 7 * 24 * 3600
@@ -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
@@ -358,8 +416,17 @@ Reputable.track_request_async(
358
416
  ### Reputation Management
359
417
 
360
418
  ```ruby
361
- # Trust IP forever (after payment, verification, etc.)
362
- Reputable.trust_ip(request.ip, reason: 'payment_completed', order_id: order.id)
419
+ # Trust IP (behavioral by default, uses default TTL)
420
+ Reputable.trust_ip(request.ip, reason: 'behavior_trust', order_id: order.id)
421
+
422
+ # Trust IP as verified (forever, explicitly)
423
+ Reputable.trust_ip(
424
+ request.ip,
425
+ reason: 'payment_completed',
426
+ status: :trusted_verified,
427
+ ttl: 0,
428
+ order_id: order.id
429
+ )
363
430
 
364
431
  # Challenge (require CAPTCHA, etc.)
365
432
  Reputable.challenge_ip(request.ip, reason: 'unusual_activity')
data/RELEASING.md ADDED
@@ -0,0 +1,26 @@
1
+ # Releasing the Ruby Gem
2
+
3
+ ## Update version + changelog
4
+
5
+ 1. Bump the version in `clients/ruby/lib/reputable/version.rb`.
6
+ 2. Add a matching entry in `CHANGELOG.md` (repo root).
7
+
8
+ ## Build the gem (optional local check)
9
+
10
+ ```bash
11
+ cd clients/ruby
12
+ bundle exec rake build
13
+ ```
14
+
15
+ ## Build + push from repo root (npm script)
16
+
17
+ ```bash
18
+ npm run release:gem
19
+ ```
20
+
21
+ ## Publish (if applicable)
22
+
23
+ ```bash
24
+ cd clients/ruby
25
+ gem push pkg/reputable-<version>.gem
26
+ ```
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reputable
4
+ # Simple HTML blocked page renderer for Rack/Rails usage.
5
+ module BlockedPage
6
+ class << self
7
+ def response(
8
+ site_name: nil,
9
+ support_email: nil,
10
+ support_url: nil,
11
+ client_ip: nil,
12
+ heading: "Access blocked",
13
+ message: nil,
14
+ status: 403,
15
+ show_ip: true
16
+ )
17
+ html = build_html(
18
+ site_name: site_name,
19
+ support_email: support_email,
20
+ support_url: support_url,
21
+ client_ip: client_ip,
22
+ heading: heading,
23
+ message: message,
24
+ show_ip: show_ip
25
+ )
26
+
27
+ headers = {
28
+ "Content-Type" => "text/html; charset=utf-8",
29
+ "Cache-Control" => "no-store"
30
+ }
31
+
32
+ [status, headers, [html]]
33
+ end
34
+
35
+ def html(**options)
36
+ build_html(**options)
37
+ end
38
+
39
+ private
40
+
41
+ def build_html(site_name:, support_email:, support_url:, client_ip:, heading:, message:, show_ip:)
42
+ display_name = escape_html(site_name || "this site")
43
+ safe_support_email = support_email ? escape_html(support_email) : nil
44
+ safe_support_url = support_url ? escape_html(support_url) : nil
45
+ safe_heading = escape_html(heading || "Access blocked")
46
+ safe_message = escape_html(
47
+ message || "We cannot allow this request for #{display_name}. Your connection was blocked by a security policy."
48
+ )
49
+
50
+ contact_line = if safe_support_email || safe_support_url
51
+ parts = []
52
+ parts << if safe_support_email
53
+ %(at <a href="mailto:#{safe_support_email}">#{safe_support_email}</a>)
54
+ else
55
+ nil
56
+ end
57
+ parts << if safe_support_url
58
+ %(via <a href="#{safe_support_url}">support</a>)
59
+ else
60
+ nil
61
+ end
62
+ parts.compact.join(" or ")
63
+ else
64
+ "by contacting the site owner"
65
+ end
66
+
67
+ ip_line = if show_ip && client_ip && !client_ip.to_s.empty?
68
+ %(<div class="ip-text">Your IP: #{escape_html(client_ip.to_s)}</div>)
69
+ else
70
+ ""
71
+ end
72
+
73
+ <<~HTML
74
+ <!DOCTYPE html>
75
+ <html lang="en">
76
+ <head>
77
+ <meta charset="UTF-8">
78
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
79
+ <title>Access blocked</title>
80
+ <style>
81
+ :root {
82
+ --bg-color: #fafafa;
83
+ --card-bg: #ffffff;
84
+ --text-color: #333333;
85
+ --text-muted: #6b7280;
86
+ --border-color: #e5e7eb;
87
+ --link-color: #2563eb;
88
+ }
89
+ @media (prefers-color-scheme: dark) {
90
+ :root {
91
+ --bg-color: #0f0f0f;
92
+ --card-bg: #1a1a1a;
93
+ --text-color: #f3f4f6;
94
+ --text-muted: #9ca3af;
95
+ --border-color: #374151;
96
+ --link-color: #60a5fa;
97
+ }
98
+ }
99
+ * { box-sizing: border-box; }
100
+ body {
101
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
102
+ background: var(--bg-color);
103
+ color: var(--text-color);
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ min-height: 100vh;
108
+ margin: 0;
109
+ padding: 20px;
110
+ }
111
+ .card {
112
+ max-width: 520px;
113
+ width: 100%;
114
+ padding: 40px;
115
+ background: var(--card-bg);
116
+ border: 1px solid var(--border-color);
117
+ border-radius: 12px;
118
+ text-align: center;
119
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1);
120
+ }
121
+ h1 { margin: 0 0 12px; font-size: 22px; font-weight: 600; }
122
+ p { margin: 0 0 20px; line-height: 1.5; color: var(--text-muted); }
123
+ a { color: var(--link-color); text-decoration: none; }
124
+ a:hover { text-decoration: underline; }
125
+ .muted { font-size: 13px; color: var(--text-muted); }
126
+ .branding {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ gap: 8px;
131
+ margin-top: 24px;
132
+ padding-top: 20px;
133
+ border-top: 1px solid var(--border-color);
134
+ }
135
+ .branding-text {
136
+ font-size: 12px;
137
+ color: var(--text-muted);
138
+ }
139
+ .ip-text {
140
+ margin-top: 12px;
141
+ font-size: 11px;
142
+ color: var(--text-muted);
143
+ }
144
+ </style>
145
+ </head>
146
+ <body>
147
+ <div class="card">
148
+ <h1>#{safe_heading}</h1>
149
+ <p>#{safe_message}</p>
150
+ <p class="muted">If you believe this is a mistake, you can reach #{display_name} #{contact_line}.</p>
151
+ <div class="branding">
152
+ <span class="branding-text">Protected by Reputable</span>
153
+ </div>
154
+ #{ip_line}
155
+ </div>
156
+ </body>
157
+ </html>
158
+ HTML
159
+ end
160
+
161
+ def escape_html(text)
162
+ text.to_s
163
+ .gsub("&", "&amp;")
164
+ .gsub("<", "&lt;")
165
+ .gsub(">", "&gt;")
166
+ .gsub('"', "&quot;")
167
+ .gsub("'", "&#39;")
168
+ end
169
+ end
170
+ end
171
+ end
@@ -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
@@ -23,7 +24,7 @@ module Reputable
23
24
  # Default TTLs in seconds (0 = forever)
24
25
  DEFAULT_TTLS = {
25
26
  trusted_verified: 0, # Forever
26
- trusted_behavior: 30 * 24 * 3600, # 30 days
27
+ trusted_behavior: 365 * 24 * 3600, # 1 year
27
28
  untrusted_challenge: 7 * 24 * 3600, # 7 days
28
29
  untrusted_block: 7 * 24 * 3600, # 7 days
29
30
  untrusted_ignore: 7 * 24 * 3600 # 7 days
@@ -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
 
@@ -25,11 +25,13 @@ module Reputable
25
25
  )
26
26
  end
27
27
 
28
- # Trust the current user's IP (e.g., after payment)
29
- def trust_current_ip(reason:, **metadata)
28
+ # Trust the current user's IP (behavioral by default, verified optional)
29
+ def trust_current_ip(reason:, status: :trusted_behavior, ttl: nil, **metadata)
30
30
  Reputable::Reputation.trust_ip(
31
31
  request.remote_ip,
32
32
  reason: reason,
33
+ status: status,
34
+ ttl: ttl,
33
35
  **metadata
34
36
  )
35
37
  end
@@ -90,6 +92,30 @@ module Reputable
90
92
  # Verification redirect helpers
91
93
  # ========================================
92
94
 
95
+ # Render the standard blocked page
96
+ def render_reputable_blocked_page(
97
+ status: 403,
98
+ site_name: nil,
99
+ support_email: nil,
100
+ support_url: nil,
101
+ heading: "Access blocked",
102
+ message: nil,
103
+ show_ip: true
104
+ )
105
+ config = Reputable.configuration
106
+ html = Reputable::BlockedPage.html(
107
+ site_name: site_name || config.site_name,
108
+ support_email: support_email || config.support_email,
109
+ support_url: support_url || config.support_url,
110
+ client_ip: request.remote_ip,
111
+ heading: heading,
112
+ message: message,
113
+ show_ip: show_ip
114
+ )
115
+
116
+ render html: html.html_safe, status: status, content_type: "text/html"
117
+ end
118
+
93
119
  # Check if the current session has already passed verification
94
120
  # @param session_key [Symbol]
95
121
  # @return [Boolean]
@@ -143,6 +169,47 @@ module Reputable
143
169
  ) and return
144
170
  end
145
171
 
172
+ # Enforce reputation-based access control.
173
+ # - If IP is blocked, render a blocked page.
174
+ # - If IP is challenged, run verification flow.
175
+ def require_reputable_reputation!(
176
+ return_url: request.original_url,
177
+ failure_url: nil,
178
+ session_id: session.id,
179
+ force_challenge: false,
180
+ session_key: :reputable_verified_at,
181
+ blocked_status: 403,
182
+ blocked_site_name: nil,
183
+ blocked_support_email: nil,
184
+ blocked_support_url: nil,
185
+ blocked_heading: "Access blocked",
186
+ blocked_message: nil,
187
+ blocked_show_ip: true
188
+ )
189
+ if current_ip_blocked?
190
+ render_reputable_blocked_page(
191
+ status: blocked_status,
192
+ site_name: blocked_site_name,
193
+ support_email: blocked_support_email,
194
+ support_url: blocked_support_url,
195
+ heading: blocked_heading,
196
+ message: blocked_message,
197
+ show_ip: blocked_show_ip
198
+ )
199
+ return
200
+ end
201
+
202
+ return unless current_ip_challenged?
203
+
204
+ require_reputable_verification!(
205
+ return_url: return_url,
206
+ failure_url: failure_url,
207
+ session_id: session_id,
208
+ force_challenge: force_challenge,
209
+ session_key: session_key
210
+ )
211
+ end
212
+
146
213
  private
147
214
 
148
215
  def reputable_verification_url(return_url:, failure_url:, session_id:, force_challenge:)
@@ -62,14 +62,16 @@ module Reputable
62
62
  false
63
63
  end
64
64
 
65
- # Convenience method: Trust an IP (verified status, forever TTL)
66
- def trust_ip(ip, reason: "manual_trust", **metadata)
65
+ # Convenience method: Trust an IP (behavioral by default)
66
+ # @param status [Symbol] :trusted_behavior or :trusted_verified
67
+ # @param ttl [Integer, nil] TTL in seconds (0 = forever, nil = use default)
68
+ def trust_ip(ip, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
67
69
  apply(
68
70
  entity_type: :ip,
69
71
  entity_id: ip,
70
- status: :trusted_verified,
72
+ status: status,
71
73
  reason: reason,
72
- ttl: 0,
74
+ ttl: ttl,
73
75
  metadata: metadata
74
76
  )
75
77
  end
@@ -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.10"
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)
@@ -33,8 +34,8 @@ end
33
34
  # path: "/products/123"
34
35
  # )
35
36
  #
36
- # @example Apply reputation after payment
37
- # Reputable.trust_ip("203.0.113.45", reason: "payment_completed")
37
+ # @example Apply verified reputation after payment
38
+ # Reputable.trust_ip("203.0.113.45", reason: "payment_completed", status: :trusted_verified, ttl: 0)
38
39
  #
39
40
  # @example Disable completely via ENV
40
41
  # # In your environment: REPUTABLE_ENABLED=false
@@ -88,8 +89,8 @@ module Reputable
88
89
  Reputation.apply(entity_type: entity_type, entity_id: entity_id, status: status, **options)
89
90
  end
90
91
 
91
- def trust_ip(ip, reason: "manual_trust", **metadata)
92
- Reputation.trust_ip(ip, reason: reason, **metadata)
92
+ def trust_ip(ip, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
93
+ Reputation.trust_ip(ip, reason: reason, status: status, ttl: ttl, **metadata)
93
94
  end
94
95
 
95
96
  def block_ip(ip, reason: "manual_block", **metadata)
@@ -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.10
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-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -126,8 +126,10 @@ files:
126
126
  - Gemfile
127
127
  - Gemfile.lock
128
128
  - README.md
129
+ - RELEASING.md
129
130
  - Rakefile
130
131
  - lib/reputable.rb
132
+ - lib/reputable/blocked_page.rb
131
133
  - lib/reputable/configuration.rb
132
134
  - lib/reputable/connection.rb
133
135
  - lib/reputable/middleware.rb
@@ -135,6 +137,7 @@ files:
135
137
  - lib/reputable/reputation.rb
136
138
  - lib/reputable/tracker.rb
137
139
  - lib/reputable/version.rb
140
+ - reputable.gemspec
138
141
  homepage: https://github.com/reputable-click/reputable-rb
139
142
  licenses:
140
143
  - MIT