reputable 0.1.9 → 0.1.11

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: e8d9854d3f0dfbf85029fa9411273ffdebad9288eaa11b91649a3aec538b6b2b
4
- data.tar.gz: 8d7fbfaf636c06c80a1ea291a773d35c1e478f5dd1261efd6e213a473f1d0362
3
+ metadata.gz: 342aa90209deb951c9e527396dfc48b1609603ac305bac088c6d20846c1a6dee
4
+ data.tar.gz: 65a4a67057b29ee2c787788393e7803302e958397974b53685a792cdc668718a
5
5
  SHA512:
6
- metadata.gz: a13fdb389753a6aed4bde2d9e6bc2304e184db0ded7a579adc6d7fef16d4e1e72a6032f11eec7e6e03e0e706aa2a135c07c17e3e607e227f7670864e84c91aea
7
- data.tar.gz: fb215dbd673959e3414eaa31c0a77a3c78366db796f34b1afbef12d066041b1308b0c6312a90dc7ee953a8b578083192509a535fb3bf9b53f265f9f3b9a22f80
6
+ metadata.gz: f410554eaf31c3386094003f72665d9b7b6020b68c91d67ef4626589708ea1fe47bb01f975971a282f0e7e08dadc9384ae8babf816dec1ee1a8febbc0668ecc9
7
+ data.tar.gz: 244d984492e5a91987e17ceadfe5e67e88beba0280c5a56057cf112768f9c981738ae72b63c91da93dce80c92c56c86a7ad9af220f914a791c6dc86844915af6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reputable (0.1.9)
4
+ reputable (0.1.11)
5
5
  connection_pool (~> 2.2)
6
6
  redis (>= 4.0, < 6.0)
7
7
 
data/README.md CHANGED
@@ -129,7 +129,7 @@ Reputable.configure do |config|
129
129
  # Customize TTLs (in seconds, 0 = forever)
130
130
  config.default_ttls = {
131
131
  trusted_verified: 0, # Forever
132
- trusted_behavior: 30 * 24 * 3600, # 30 days
132
+ trusted_behavior: 365 * 24 * 3600, # 1 year
133
133
  untrusted_challenge: 7 * 24 * 3600,
134
134
  untrusted_block: 7 * 24 * 3600,
135
135
  untrusted_ignore: 7 * 24 * 3600
@@ -269,7 +269,11 @@ config.middleware.use Reputable::Middleware,
269
269
  },
270
270
 
271
271
  # Async mode (default: true) - tracking runs in background thread
272
- async: true
272
+ async: true,
273
+
274
+ # Expose reputation flags in request env for views/controllers (default: true)
275
+ # Sets request.env['reputable.ignore_analytics'] when status is untrusted_ignore
276
+ expose_reputation: true
273
277
  ```
274
278
 
275
279
  ### Optional Reputation Gate
@@ -328,6 +332,11 @@ if current_ip_trusted?
328
332
  # Skip CAPTCHA, higher rate limits
329
333
  end
330
334
 
335
+ # View/helper flag for untrusted_ignore
336
+ if reputable_ignore_analytics?
337
+ # Skip analytics / tracking in views
338
+ end
339
+
331
340
  if current_ip_blocked?
332
341
  render status: 403
333
342
  return
@@ -416,8 +425,17 @@ Reputable.track_request_async(
416
425
  ### Reputation Management
417
426
 
418
427
  ```ruby
419
- # Trust IP forever (after payment, verification, etc.)
420
- Reputable.trust_ip(request.ip, reason: 'payment_completed', order_id: order.id)
428
+ # Trust IP (behavioral by default, uses default TTL)
429
+ Reputable.trust_ip(request.ip, reason: 'behavior_trust', order_id: order.id)
430
+
431
+ # Trust IP as verified (forever, explicitly)
432
+ Reputable.trust_ip(
433
+ request.ip,
434
+ reason: 'payment_completed',
435
+ status: :trusted_verified,
436
+ ttl: 0,
437
+ order_id: order.id
438
+ )
421
439
 
422
440
  # Challenge (require CAPTCHA, etc.)
423
441
  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
@@ -24,7 +24,7 @@ module Reputable
24
24
  # Default TTLs in seconds (0 = forever)
25
25
  DEFAULT_TTLS = {
26
26
  trusted_verified: 0, # Forever
27
- trusted_behavior: 30 * 24 * 3600, # 30 days
27
+ trusted_behavior: 365 * 24 * 3600, # 1 year
28
28
  untrusted_challenge: 7 * 24 * 3600, # 7 days
29
29
  untrusted_block: 7 * 24 * 3600, # 7 days
30
30
  untrusted_ignore: 7 * 24 * 3600 # 7 days
@@ -45,6 +45,7 @@ module Reputable
45
45
  @tag_builder = options[:tag_builder]
46
46
  @async = options.fetch(:async, true)
47
47
  @reputation_gate = options.fetch(:reputation_gate, false)
48
+ @expose_reputation = options.fetch(:expose_reputation, true)
48
49
  @challenge_action = options.fetch(:challenge_action, :verify)
49
50
  @block_action = options.fetch(:block_action, :blocked_page_remote)
50
51
  @challenge_redirect_status = options.fetch(:challenge_redirect_status, 302)
@@ -72,6 +73,9 @@ module Reputable
72
73
  return gate_response
73
74
  end
74
75
 
76
+ # Optional: expose reputation context for views/controllers
77
+ safe_apply_reputation_context(env) if @expose_reputation
78
+
75
79
  # ALWAYS process the request first - tracking must never block
76
80
  status, headers, response = @app.call(env)
77
81
 
@@ -96,9 +100,9 @@ module Reputable
96
100
  end
97
101
 
98
102
  def enforce_reputation_gate(env)
99
- ip = extract_ip(env)
100
- status = Reputable::Reputation.lookup_ip(ip)
103
+ status = reputation_status(env)
101
104
  return nil if status.nil?
105
+ ip = env["reputable.ip"] || extract_ip(env)
102
106
 
103
107
  case status
104
108
  when "untrusted_block"
@@ -133,6 +137,10 @@ module Reputable
133
137
 
134
138
  if Reputable.verify_redirect_return(request.params)
135
139
  env["reputable.verified"] = true
140
+ ignore_analytics = request.params["reputable_ignore_analytics"]
141
+ unless ignore_analytics.nil?
142
+ env["reputable.ignore_analytics"] = ignore_analytics.to_s == "true"
143
+ end
136
144
 
137
145
  # Store in session if available
138
146
  if env["rack.session"]
@@ -255,6 +263,30 @@ module Reputable
255
263
  Reputable::BlockedPage.response(**options)
256
264
  end
257
265
 
266
+ def safe_apply_reputation_context(env)
267
+ return unless Reputable.enabled?
268
+ return if skip_request?(env)
269
+
270
+ reputation_status(env)
271
+ rescue StandardError => e
272
+ Reputable.logger&.debug("Reputable reputation context: #{e.class} - #{e.message}")
273
+ end
274
+
275
+ def reputation_status(env)
276
+ return env["reputable.reputation_status"] if env.key?("reputable.reputation_status")
277
+
278
+ ip = extract_ip(env)
279
+ env["reputable.ip"] = ip
280
+ status = Reputable::Reputation.lookup_ip(ip)
281
+ env["reputable.reputation_status"] = status
282
+ env["reputable.ignore_analytics"] = (status == "untrusted_ignore")
283
+ status
284
+ rescue StandardError
285
+ env["reputable.reputation_status"] = nil
286
+ env["reputable.ignore_analytics"] = false
287
+ nil
288
+ end
289
+
258
290
  def blocked_page_options
259
291
  config = Reputable.configuration
260
292
  defaults = {
@@ -8,7 +8,7 @@ module Reputable
8
8
  extend ActiveSupport::Concern
9
9
 
10
10
  included do
11
- helper_method :reputable_verified? if respond_to?(:helper_method)
11
+ helper_method :reputable_verified?, :reputable_ignore_analytics? if respond_to?(:helper_method)
12
12
  end
13
13
 
14
14
  # Track the current request with optional extra tags
@@ -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
@@ -86,6 +88,17 @@ module Reputable
86
88
  Reputable::Reputation.lookup_ip(request.remote_ip)
87
89
  end
88
90
 
91
+ # Check if analytics should be ignored for this request
92
+ # Uses middleware-populated flag when available, falls back to lookup.
93
+ def reputable_ignore_analytics?
94
+ value = request.env["reputable.ignore_analytics"]
95
+ return value unless value.nil?
96
+
97
+ current_ip_status == "untrusted_ignore"
98
+ rescue StandardError
99
+ false
100
+ end
101
+
89
102
  # ========================================
90
103
  # Verification redirect helpers
91
104
  # ========================================
@@ -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.9"
4
+ VERSION = "0.1.11"
5
5
  end
data/lib/reputable.rb CHANGED
@@ -34,8 +34,8 @@ end
34
34
  # path: "/products/123"
35
35
  # )
36
36
  #
37
- # @example Apply reputation after payment
38
- # 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)
39
39
  #
40
40
  # @example Disable completely via ENV
41
41
  # # In your environment: REPUTABLE_ENABLED=false
@@ -89,8 +89,8 @@ module Reputable
89
89
  Reputation.apply(entity_type: entity_type, entity_id: entity_id, status: status, **options)
90
90
  end
91
91
 
92
- def trust_ip(ip, reason: "manual_trust", **metadata)
93
- 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)
94
94
  end
95
95
 
96
96
  def block_ip(ip, reason: "manual_block", **metadata)
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.9
4
+ version: 0.1.11
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-29 00:00:00.000000000 Z
11
+ date: 2025-12-31 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