reputable 0.1.1 → 0.1.3

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: 58cd625b57ec78df69ca3a6f46e810f3870d30d2c708c61ded9c028737eef1e8
4
- data.tar.gz: b73a0ad0cfffead0089d870bfd51dee53b3ee4af4305992ec9285f37f0e18e4b
3
+ metadata.gz: ca78832e420ad0cacad0c44bbfbf2a682f1b9f702bbaf8aa07a44e4e04d7d53b
4
+ data.tar.gz: 9e15f534984a4e3d67560a98cbede0c596d02d30fb7f7171b380b043b0781384
5
5
  SHA512:
6
- metadata.gz: 8e99d9b81e8fe4ee5594ad30637eac3387a89e4073644ae8a54a25c211e6c2a0cae9dcf38b03a6767474845f0b63437e719a2f9bc4748b208afe617c3d43a9e5
7
- data.tar.gz: 5e6e0d1a50150f7e5acec89be721be877ee17e1a1bb9f388c5ab4c70b14e9b2a6efcd816087e4a177e712723081550d7af704cc8f212961131f92cfdc8063de8
6
+ metadata.gz: 23832e31d409bb3316c33c931e6207f24eb2ea078c2fa79d3d0c4618e4ffd3359f525d0c6b090344bb5d8e79cf9fdc714752ae09180ce973dd353d93015afb6c
7
+ data.tar.gz: 9eb4a989b111a493e7555672057946c2928251a1112e7b75e011bf7183c329ee3b9c6c6cb0e34a163d3803cb5956321b443b25f2d9fcbfca5c90ad16b73cd766
data/.DS_Store ADDED
Binary file
data/Gemfile.lock ADDED
@@ -0,0 +1,76 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ reputable (0.1.3)
5
+ connection_pool (~> 2.2)
6
+ redis (>= 4.0, < 6.0)
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ ast (2.4.3)
12
+ connection_pool (2.5.5)
13
+ diff-lcs (1.6.2)
14
+ json (2.18.0)
15
+ language_server-protocol (3.17.0.5)
16
+ lint_roller (1.1.0)
17
+ parallel (1.27.0)
18
+ parser (3.3.10.0)
19
+ ast (~> 2.4.1)
20
+ racc
21
+ prism (1.7.0)
22
+ racc (1.8.1)
23
+ rack (2.2.21)
24
+ rainbow (3.1.1)
25
+ rake (13.3.1)
26
+ redis (5.4.1)
27
+ redis-client (>= 0.22.0)
28
+ redis-client (0.26.2)
29
+ connection_pool
30
+ regexp_parser (2.11.3)
31
+ rspec (3.13.2)
32
+ rspec-core (~> 3.13.0)
33
+ rspec-expectations (~> 3.13.0)
34
+ rspec-mocks (~> 3.13.0)
35
+ rspec-core (3.13.6)
36
+ rspec-support (~> 3.13.0)
37
+ rspec-expectations (3.13.5)
38
+ diff-lcs (>= 1.2.0, < 2.0)
39
+ rspec-support (~> 3.13.0)
40
+ rspec-mocks (3.13.7)
41
+ diff-lcs (>= 1.2.0, < 2.0)
42
+ rspec-support (~> 3.13.0)
43
+ rspec-support (3.13.6)
44
+ rubocop (1.82.0)
45
+ json (~> 2.3)
46
+ language_server-protocol (~> 3.17.0.2)
47
+ lint_roller (~> 1.1.0)
48
+ parallel (~> 1.10)
49
+ parser (>= 3.3.0.2)
50
+ rainbow (>= 2.2.2, < 4.0)
51
+ regexp_parser (>= 2.9.3, < 3.0)
52
+ rubocop-ast (>= 1.48.0, < 2.0)
53
+ ruby-progressbar (~> 1.7)
54
+ unicode-display_width (>= 2.4.0, < 4.0)
55
+ rubocop-ast (1.48.0)
56
+ parser (>= 3.3.7.2)
57
+ prism (~> 1.4)
58
+ ruby-progressbar (1.13.0)
59
+ unicode-display_width (3.2.0)
60
+ unicode-emoji (~> 4.1)
61
+ unicode-emoji (4.2.0)
62
+
63
+ PLATFORMS
64
+ arm64-darwin-25
65
+ ruby
66
+
67
+ DEPENDENCIES
68
+ bundler (~> 2.0)
69
+ rack (~> 2.0)
70
+ rake (~> 13.0)
71
+ reputable!
72
+ rspec (~> 3.0)
73
+ rubocop (~> 1.21)
74
+
75
+ BUNDLED WITH
76
+ 2.5.9
data/README.md CHANGED
@@ -28,9 +28,6 @@ Reputable.configure do |config|
28
28
  # Required: Redis/Dragonfly URL (TLS supported via rediss://)
29
29
  config.redis_url = ENV['REPUTABLE_REDIS_URL']
30
30
 
31
- # Required: Your tenant ID
32
- config.tenant_id = ENV['REPUTABLE_TENANT_ID']
33
-
34
31
  # Optional: Enable logging (logs at debug level)
35
32
  Reputable.logger = Rails.logger
36
33
  end
@@ -82,7 +79,6 @@ All configuration can be set via environment variables:
82
79
  ```bash
83
80
  # Required
84
81
  REPUTABLE_REDIS_URL=rediss://user:password@your-dragonfly.example.com:6379
85
- REPUTABLE_TENANT_ID=your-tenant-id
86
82
 
87
83
  # Optional: Disable entirely (useful for test environments)
88
84
  REPUTABLE_ENABLED=false
@@ -105,9 +101,6 @@ Reputable.configure do |config|
105
101
  # Redis connection (supports redis:// and rediss:// for TLS)
106
102
  config.redis_url = ENV['REPUTABLE_REDIS_URL']
107
103
 
108
- # Your tenant identifier
109
- config.tenant_id = ENV['REPUTABLE_TENANT_ID']
110
-
111
104
  # Connection pool settings
112
105
  config.pool_size = 5 # Number of Redis connections
113
106
  config.pool_timeout = 1.0 # Max wait for connection (seconds)
@@ -142,6 +135,11 @@ Reputable.configure do |config|
142
135
  REMOTE_ADDR
143
136
  ]
144
137
 
138
+ # Verification Configuration
139
+ # Supports comma-separated list in REPUTABLE_TRUSTED_KEYS or single key in REPUTABLE_TRUSTED_KEY
140
+ config.trusted_keys = ENV['REPUTABLE_TRUSTED_KEYS']&.split(',') || ENV['REPUTABLE_TRUSTED_KEY']
141
+ config.verification_base_url = ENV['REPUTABLE_VERIFY_URL'] # URL of your Reputable API verify endpoint
142
+
145
143
  # Error callback (optional)
146
144
  config.on_error = ->(error, context) {
147
145
  # Report to your error tracking service
@@ -284,8 +282,6 @@ track_reputable_request(tags: ['custom:tag'])
284
282
 
285
283
  # Trust methods (after successful actions)
286
284
  trust_current_ip(reason: 'payment_completed', order_id: '123')
287
- trust_current_user(reason: 'email_verified')
288
- trust_current_session(reason: 'captcha_passed')
289
285
 
290
286
  # Challenge/Block methods
291
287
  challenge_current_ip(reason: 'suspicious_activity')
@@ -319,11 +315,9 @@ Reputable.track_request(
319
315
  path: request.path,
320
316
  query: request.query_string,
321
317
  method: request.request_method,
322
- session_id: session.id,
323
- session_present: true,
324
318
  user_agent: request.user_agent,
325
319
  referer: request.referer,
326
- tags: ['ctx:page:product']
320
+ tags: ['view:page:product']
327
321
  )
328
322
 
329
323
  # Asynchronous (fire-and-forget, recommended)
@@ -339,12 +333,6 @@ Reputable.track_request_async(
339
333
  # Trust IP forever (after payment, verification, etc.)
340
334
  Reputable.trust_ip(request.ip, reason: 'payment_completed', order_id: order.id)
341
335
 
342
- # Trust a user
343
- Reputable.trust_user(current_user.id, reason: 'email_verified')
344
-
345
- # Trust a session (default: 24 hour TTL)
346
- Reputable.trust_session(session.id, reason: 'captcha_passed')
347
-
348
336
  # Challenge (require CAPTCHA, etc.)
349
337
  Reputable.challenge_ip(request.ip, reason: 'unusual_activity')
350
338
 
@@ -372,12 +360,80 @@ Reputable.lookup_reputation(:ip, request.ip)
372
360
  # => { status: "trusted_verified", reason: "payment_completed",
373
361
  # source: "app_server", updated_at: 1703123456789,
374
362
  # expires_at: 0, metadata: { order_id: "123" } }
363
+ ```
364
+
365
+ ---
366
+
367
+ ## User Verification & Trust Flow
368
+
369
+ When you identify a suspicious user (e.g., high risk score or specific tag), you can redirect them to the Reputable verification page. This page performs advanced browser checks and challenges (CAPTCHA) if necessary.
370
+
371
+ ### 1. Generating a Signed Redirect URL
372
+
373
+ To effectively hand off verification handling to Reputable, generate a signed verification URL:
374
+
375
+ ```ruby
376
+ # In your controller
377
+ if suspicious_activity_detected?
378
+ redirect_url = Reputable.verification_url(
379
+ return_url: request.original_url, # Where to send them back after verification
380
+ failure_url: root_url, # Optional: where to send if they fail/garbage token
381
+ session_id: session.id # Optional: link specific session
382
+ )
383
+
384
+ redirect_to redirect_url
385
+ end
386
+ ```
387
+
388
+ ### 2. Handling the Return Redirect
389
+
390
+ When the user passes verification (or is determined to be already trusted/clean), they are immediately redirected back to your `return_url` with signed parameters.
391
+
392
+ **Middleware (Automatic Handling):**
393
+ If you are using `Reputable::Middleware` (recommended), this is handled automatically. The middleware detects the return parameters, verifies the signature, and sets `env['reputable.verified'] = true`.
394
+
395
+ ```ruby
396
+ # In your controller
397
+ if request.env['reputable.verified']
398
+ # User just passed verification!
399
+ # You might want to graduate them to trusted status locally
400
+ trust_current_ip(reason: 'interactive_verification')
401
+ end
402
+ ```
375
403
 
376
- # User/Session lookups
377
- Reputable.trusted_user?(current_user.id)
378
- Reputable.trusted_session?(session.id)
404
+ **Manual Verification:**
405
+ If you need to verify manually (e.g., custom controller logic):
406
+
407
+ ```ruby
408
+ # In your controller action (the return_url target)
409
+ if params[:reputable_signature]
410
+ if Reputable.verify_redirect_return(params)
411
+ # Signature is VALID. Meaning Reputable actually sent this user back.
412
+
413
+ # Inspect outcomes
414
+ status = params[:reputable_status] # 'pass', 'fail'
415
+ outcome = params[:reputable_outcome] # 'trusted_verified', 'trusted_behavior', 'unknown'
416
+ country = params[:reputable_country] # 'US', 'DE', etc.
417
+
418
+ if status == 'pass'
419
+ # Grant access
420
+ end
421
+ else
422
+ # Invalid signature - potential tampering attempt!
423
+ render plain: "Verification failed", status: 403
424
+ end
425
+ end
379
426
  ```
380
427
 
428
+ **Return Parameters:**
429
+ The return URL will contain:
430
+ - `reputable_status`: `pass` or `fail`
431
+ - `reputable_session_id`: The session ID you provided
432
+ - `reputable_outcome`: The specific reputation outcome (e.g., `trusted_verified`)
433
+ - `reputable_ignore_analytics`: 'true'/'false' flag
434
+ - `reputable_country`: ISO country code
435
+ - `reputable_signature`: HMAC-SHA256 signature of the above
436
+
381
437
  ---
382
438
 
383
439
  ## Resilience & Failsafe Features
@@ -438,10 +494,10 @@ Use tags to classify requests for behavioral analysis:
438
494
 
439
495
  ```ruby
440
496
  # Page context
441
- tags: ['ctx:page:product']
442
- tags: ['ctx:page:checkout']
443
- tags: ['ctx:page:cart']
444
- tags: ['ctx:page:login']
497
+ tags: ['view:page:product']
498
+ tags: ['view:page:checkout']
499
+ tags: ['view:page:cart']
500
+ tags: ['view:page:login']
445
501
 
446
502
  # Traffic source
447
503
  tags: ['trust:channel:email']
@@ -493,7 +549,6 @@ expect(Reputable.lookup_ip('1.2.3.4')).to be_nil
493
549
  ### What's Available from Rails
494
550
 
495
551
  - IP reputation and history
496
- - Session tracking
497
552
  - Request classification
498
553
  - UA churn detection
499
554
  - Cross-request pattern analysis
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -9,12 +9,12 @@ module Reputable
9
9
  # Supports TLS connections with automatic SSL error handling.
10
10
  # All SSL/connection errors are caught and logged, never breaking your app.
11
11
  class Configuration
12
- attr_accessor :redis_url, :redis_options, :tenant_id, :buffer_prefix,
12
+ attr_accessor :redis_url, :redis_options, :buffer_prefix,
13
13
  :request_buffer_key, :reputation_buffer_key,
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
17
+ :on_error, :trusted_keys, :verification_base_url
18
18
 
19
19
  # Default TTLs in seconds (0 = forever)
20
20
  DEFAULT_TTLS = {
@@ -52,7 +52,6 @@ module Reputable
52
52
  def initialize
53
53
  @redis_url = ENV.fetch("REPUTABLE_REDIS_URL", "redis://127.0.0.1:6379")
54
54
  @redis_options = {}
55
- @tenant_id = ENV.fetch("REPUTABLE_TENANT_ID", "default")
56
55
  @buffer_prefix = "reputable:buffer"
57
56
  @pool_size = Integer(ENV.fetch("REPUTABLE_POOL_SIZE", "5"))
58
57
  @pool_timeout = Float(ENV.fetch("REPUTABLE_POOL_TIMEOUT", "1.0"))
@@ -64,6 +63,34 @@ module Reputable
64
63
  @trusted_proxies = nil # Additional trusted proxy IPs/ranges
65
64
  @ip_header_priority = DEFAULT_IP_HEADERS.dup
66
65
  @on_error = nil # Optional error callback: ->(error, context) { ... }
66
+
67
+ # Support multiple trusted keys (comma-separated), fallback to old secret key env var
68
+ @trusted_keys = []
69
+ if ENV["REPUTABLE_TRUSTED_KEYS"]
70
+ @trusted_keys = ENV["REPUTABLE_TRUSTED_KEYS"].split(",").map(&:strip)
71
+ elsif ENV["REPUTABLE_TRUSTED_KEY"]
72
+ @trusted_keys = [ENV["REPUTABLE_TRUSTED_KEY"]]
73
+ elsif ENV["REPUTABLE_SECRET_KEY"]
74
+ @trusted_keys = [ENV["REPUTABLE_SECRET_KEY"]]
75
+ end
76
+ @verification_base_url = ENV.fetch("REPUTABLE_VERIFY_URL", "https://api.reputable.click/_reputable/verify")
77
+ end
78
+
79
+ # Alias for backward compatibility
80
+ def secret_key
81
+ @trusted_keys.first
82
+ end
83
+
84
+ def secret_key=(key)
85
+ @trusted_keys = [key]
86
+ end
87
+
88
+ def trusted_keys=(keys)
89
+ @trusted_keys = Array(keys)
90
+ end
91
+
92
+ def trusted_keys
93
+ @trusted_keys
67
94
  end
68
95
 
69
96
  # Check if Reputable is enabled (can be disabled via ENV)
@@ -75,11 +102,11 @@ module Reputable
75
102
  end
76
103
 
77
104
  def request_buffer_key
78
- @request_buffer_key || "#{buffer_prefix}:requests:#{tenant_id}"
105
+ @request_buffer_key || "#{buffer_prefix}:requests"
79
106
  end
80
107
 
81
108
  def reputation_buffer_key
82
- @reputation_buffer_key || "#{buffer_prefix}:reputation:#{tenant_id}"
109
+ @reputation_buffer_key || "#{buffer_prefix}:reputation"
83
110
  end
84
111
 
85
112
  def default_ttl_for(status)
@@ -45,6 +45,9 @@ module Reputable
45
45
  end
46
46
 
47
47
  def call(env)
48
+ # Check for verification return parameters and verify signature if present
49
+ handle_verification_return(env)
50
+
48
51
  # ALWAYS process the request first - tracking must never block
49
52
  status, headers, response = @app.call(env)
50
53
 
@@ -70,6 +73,26 @@ module Reputable
70
73
  Reputable.logger&.debug("Reputable middleware: #{e.class} - #{e.message}")
71
74
  end
72
75
 
76
+ def handle_verification_return(env)
77
+ request = Rack::Request.new(env)
78
+ # Quick check to avoid overhead
79
+ return unless request.query_string.include?("reputable_status")
80
+
81
+ return unless request.params["reputable_status"] == "pass"
82
+
83
+ if Reputable.verify_redirect_return(request.params)
84
+ env["reputable.verified"] = true
85
+
86
+ # Store in session if available
87
+ if env["rack.session"]
88
+ env["rack.session"]["reputable_verified"] = true
89
+ env["rack.session"]["reputable_verified_at"] = Time.now.to_i
90
+ end
91
+ end
92
+ rescue StandardError => e
93
+ Reputable.logger&.debug("Reputable verification error: #{e.message}")
94
+ end
95
+
73
96
  def skip_request?(env)
74
97
  return true if @skip_if&.call(env)
75
98
 
@@ -109,8 +132,6 @@ module Reputable
109
132
  path: request.path,
110
133
  query: request.query_string.empty? ? nil : request.query_string,
111
134
  method: request.request_method,
112
- session_id: extract_session_id(env),
113
- session_present: session_present?(env),
114
135
  user_agent: env["HTTP_USER_AGENT"],
115
136
  referer: env["HTTP_REFERER"],
116
137
  tags: build_tags(env)
@@ -190,29 +211,6 @@ module Reputable
190
211
  nil
191
212
  end
192
213
 
193
- def extract_session_id(env)
194
- # Try rack.session.id first
195
- return env["rack.session.id"] if env["rack.session.id"]
196
-
197
- # Try to get from rack.session
198
- session = env["rack.session"]
199
- return session.id.to_s if session.respond_to?(:id)
200
-
201
- nil
202
- rescue StandardError
203
- nil
204
- end
205
-
206
- def session_present?(env)
207
- return true if env["rack.session.id"]
208
- return true if env["rack.session"]&.any?
209
- return true if env["HTTP_COOKIE"]&.include?("_session")
210
-
211
- false
212
- rescue StandardError
213
- false
214
- end
215
-
216
214
  def build_tags(env)
217
215
  tags = []
218
216
 
@@ -14,8 +14,6 @@ module Reputable
14
14
  path: request.path,
15
15
  query: request.query_string,
16
16
  method: request.method,
17
- session_id: session.id.to_s,
18
- session_present: session.any?,
19
17
  user_agent: request.user_agent,
20
18
  referer: request.referer,
21
19
  tags: tags,
@@ -32,26 +30,6 @@ module Reputable
32
30
  )
33
31
  end
34
32
 
35
- # Trust the current user
36
- def trust_current_user(reason:, **metadata)
37
- return false unless respond_to?(:current_user) && current_user
38
-
39
- Reputable::Reputation.trust_user(
40
- current_user.id,
41
- reason: reason,
42
- **metadata
43
- )
44
- end
45
-
46
- # Trust the current session
47
- def trust_current_session(reason:, **metadata)
48
- Reputable::Reputation.trust_session(
49
- session.id.to_s,
50
- reason: reason,
51
- **metadata
52
- )
53
- end
54
-
55
33
  # Challenge the current IP
56
34
  def challenge_current_ip(reason:, **metadata)
57
35
  Reputable::Reputation.challenge_ip(
@@ -103,20 +81,6 @@ module Reputable
103
81
  def current_ip_status
104
82
  Reputable::Reputation.lookup_ip(request.remote_ip)
105
83
  end
106
-
107
- # Check if current user is trusted
108
- # @return [Boolean]
109
- def current_user_trusted?
110
- return false unless respond_to?(:current_user) && current_user
111
-
112
- Reputable::Reputation.trusted_user?(current_user.id)
113
- end
114
-
115
- # Check if current session is trusted
116
- # @return [Boolean]
117
- def current_session_trusted?
118
- Reputable::Reputation.trusted_session?(session.id.to_s)
119
- end
120
84
  end
121
85
 
122
86
  # Railtie for automatic Rails integration (only defined when Rails is present)
@@ -130,7 +94,6 @@ module Reputable
130
94
  creds = ::Rails.application.credentials.reputable rescue nil
131
95
  if creds
132
96
  config.redis_url = creds[:redis_url] if creds[:redis_url]
133
- config.tenant_id = creds[:tenant_id] if creds[:tenant_id]
134
97
  end
135
98
  end
136
99
  end
@@ -19,12 +19,12 @@ module Reputable
19
19
  untrusted_ignore
20
20
  ].freeze
21
21
 
22
- VALID_ENTITY_TYPES = %i[ip asn ja4 session user].freeze
22
+ VALID_ENTITY_TYPES = %i[ip asn ja4].freeze
23
23
 
24
24
  class << self
25
25
  # Apply a reputation status to an entity
26
26
  #
27
- # @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4, :session, :user)
27
+ # @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4)
28
28
  # @param entity_id [String] The entity identifier (IP address, ASN, etc.)
29
29
  # @param status [Symbol] Reputation status to apply
30
30
  # @param options [Hash] Additional options
@@ -110,30 +110,6 @@ module Reputable
110
110
  )
111
111
  end
112
112
 
113
- # Trust a user (by internal user ID)
114
- def trust_user(user_id, reason: "verified_user", **metadata)
115
- apply(
116
- entity_type: :user,
117
- entity_id: user_id.to_s,
118
- status: :trusted_verified,
119
- reason: reason,
120
- ttl: 0,
121
- metadata: metadata
122
- )
123
- end
124
-
125
- # Trust a session
126
- def trust_session(session_id, reason: "verified_session", ttl: nil, **metadata)
127
- apply(
128
- entity_type: :session,
129
- entity_id: session_id,
130
- status: :trusted_verified,
131
- reason: reason,
132
- ttl: ttl || (24 * 3600), # Default 24 hours for sessions
133
- metadata: metadata
134
- )
135
- end
136
-
137
113
  # ========================================================================
138
114
  # LOOKUP METHODS (O(1) Redis hash lookups)
139
115
  # ========================================================================
@@ -141,7 +117,7 @@ module Reputable
141
117
  # Lookup the current reputation status for an entity
142
118
  # This is an O(1) Redis HGETALL operation
143
119
  #
144
- # @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4, :session, :user)
120
+ # @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4)
145
121
  # @param entity_id [String] The entity identifier
146
122
  # @return [Hash, nil] Reputation data or nil if not found/expired/error
147
123
  #
@@ -227,42 +203,6 @@ module Reputable
227
203
  lookup_ip(ip) == "untrusted_challenge"
228
204
  end
229
205
 
230
- # Lookup user reputation
231
- #
232
- # @param user_id [String, Integer] User ID
233
- # @return [String, nil] Status string or nil
234
- def lookup_user(user_id)
235
- result = lookup(:user, user_id.to_s)
236
- result&.dig(:status)
237
- end
238
-
239
- # Check if a user is trusted
240
- #
241
- # @param user_id [String, Integer] User ID
242
- # @return [Boolean]
243
- def trusted_user?(user_id)
244
- status = lookup_user(user_id)
245
- status&.start_with?("trusted")
246
- end
247
-
248
- # Lookup session reputation
249
- #
250
- # @param session_id [String] Session ID
251
- # @return [String, nil] Status string or nil
252
- def lookup_session(session_id)
253
- result = lookup(:session, session_id)
254
- result&.dig(:status)
255
- end
256
-
257
- # Check if a session is trusted
258
- #
259
- # @param session_id [String] Session ID
260
- # @return [Boolean]
261
- def trusted_session?(session_id)
262
- status = lookup_session(session_id)
263
- status&.start_with?("trusted")
264
- end
265
-
266
206
  private
267
207
 
268
208
  def valid_entity_type?(entity_type)
@@ -18,9 +18,6 @@ module Reputable
18
18
  # @param options [Hash] Additional options
19
19
  # @option options [String] :query Query string
20
20
  # @option options [String] :method HTTP method (GET, POST, etc.)
21
- # @option options [String] :session_id Session identifier
22
- # @option options [Boolean] :session_present Whether a session cookie exists
23
- # @option options [String] :user_id Internal user ID (for logged-in users)
24
21
  # @option options [String] :fingerprint Browser fingerprint hash
25
22
  # @option options [String] :user_agent User-Agent header
26
23
  # @option options [String] :referer Referer header
@@ -41,11 +38,9 @@ module Reputable
41
38
  # path: request.path,
42
39
  # query: request.query_string,
43
40
  # method: request.request_method,
44
- # session_id: session.id,
45
- # session_present: true,
46
41
  # user_agent: request.user_agent,
47
42
  # referer: request.referer,
48
- # tags: ["ctx:page:product", "trust:channel:organic"]
43
+ # tags: ["view:page:product", "trust:channel:organic"]
49
44
  # )
50
45
  def track_request(ip:, path:, **options)
51
46
  return false unless Reputable.enabled?
@@ -80,9 +75,6 @@ module Reputable
80
75
  path: path,
81
76
  query: options[:query],
82
77
  method: options[:method] || "GET",
83
- session_id: options[:session_id],
84
- session_present: options[:session_present],
85
- user_id: options[:user_id],
86
78
  fingerprint: options[:fingerprint],
87
79
  user_agent: options[:user_agent],
88
80
  referer: options[:referer],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reputable
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.3"
5
5
  end
data/lib/reputable.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "reputable/version"
4
+ require "json"
5
+ require "base64"
4
6
  require_relative "reputable/configuration"
5
7
  require_relative "reputable/connection"
6
8
  require_relative "reputable/tracker"
@@ -23,14 +25,12 @@ end
23
25
  # @example Basic setup
24
26
  # Reputable.configure do |config|
25
27
  # config.redis_url = "rediss://user:pass@dragonfly.example.com:6379"
26
- # config.tenant_id = "my-tenant"
27
28
  # end
28
29
  #
29
30
  # @example Track a request
30
31
  # Reputable.track_request(
31
32
  # ip: "203.0.113.45",
32
- # path: "/products/123",
33
- # session_present: true
33
+ # path: "/products/123"
34
34
  # )
35
35
  #
36
36
  # @example Apply reputation after payment
@@ -104,14 +104,6 @@ module Reputable
104
104
  Reputation.ignore_ip(ip, reason: reason, **metadata)
105
105
  end
106
106
 
107
- def trust_user(user_id, reason: "verified_user", **metadata)
108
- Reputation.trust_user(user_id, reason: reason, **metadata)
109
- end
110
-
111
- def trust_session(session_id, reason: "verified_session", **metadata)
112
- Reputation.trust_session(session_id, reason: reason, **metadata)
113
- end
114
-
115
107
  # Delegate lookup methods to Reputation module (O(1) Redis lookups)
116
108
  def lookup_reputation(entity_type, entity_id)
117
109
  Reputation.lookup(entity_type, entity_id)
@@ -133,20 +125,92 @@ module Reputable
133
125
  Reputation.challenged_ip?(ip)
134
126
  end
135
127
 
136
- def lookup_user(user_id)
137
- Reputation.lookup_user(user_id)
138
- end
139
-
140
- def trusted_user?(user_id)
141
- Reputation.trusted_user?(user_id)
142
- end
143
-
144
- def lookup_session(session_id)
145
- Reputation.lookup_session(session_id)
146
- end
147
-
148
- def trusted_session?(session_id)
149
- Reputation.trusted_session?(session_id)
128
+ # Generate a signed verification URL
129
+ def verification_url(return_url:, failure_url: nil, session_id: nil)
130
+ keys = configuration.trusted_keys
131
+ if keys.nil? || keys.empty?
132
+ logger&.warn "Reputable: Missing trusted_keys, cannot generate verification URL"
133
+ return return_url
134
+ end
135
+
136
+ # Use the first key for signing new requests
137
+ secret = keys.first
138
+
139
+ base_url = configuration.verification_base_url
140
+
141
+ # JWT Header
142
+ header = { alg: "HS256", typ: "JWT" }
143
+ encoded_header = base64url_encode(JSON.generate(header))
144
+
145
+ # JWT Payload
146
+ payload = {
147
+ returnUrl: return_url,
148
+ failureUrl: failure_url,
149
+ sessionId: session_id,
150
+ iat: Time.now.to_i
151
+ }
152
+ encoded_payload = base64url_encode(JSON.generate(payload))
153
+
154
+ # Signature
155
+ data = "#{encoded_header}.#{encoded_payload}"
156
+ signature = OpenSSL::HMAC.digest("SHA256", secret, data)
157
+ encoded_signature = base64url_encode(signature)
158
+
159
+ token = "#{data}.#{encoded_signature}"
160
+
161
+ "#{base_url}?token=#{token}"
162
+ end
163
+
164
+ # Verify the signature of a redirect return
165
+ # @param params [Hash] Request query parameters
166
+ # @return [Boolean] true if valid logic and passed signature check
167
+ def verify_redirect_return(params)
168
+ status = params["reputable_status"]
169
+ session_id = params["reputable_session_id"]
170
+ signature = params["reputable_signature"]
171
+ outcome = params["reputable_outcome"]
172
+ ignore_analytics = params["reputable_ignore_analytics"]
173
+ country = params["reputable_country"] || ""
174
+
175
+ return false unless status && session_id && signature
176
+
177
+ keys = configuration.trusted_keys
178
+ if keys.nil? || keys.empty?
179
+ logger&.warn "Reputable: Missing trusted_keys, cannot verify redirect"
180
+ return false
181
+ end
182
+
183
+ # Reconstruct data string: status:sessionId:outcome:ignoreAnalytics:country
184
+ # Note: optional params default to empty strings if missing in reconstruction logic on server
185
+ data_parts = [
186
+ status,
187
+ session_id,
188
+ outcome || "",
189
+ ignore_analytics.nil? ? "" : ignore_analytics,
190
+ country
191
+ ]
192
+
193
+ data = data_parts.join(":")
194
+
195
+ # Iterate through all trusted keys to find a match
196
+ keys.any? do |key|
197
+ expected_signature = OpenSSL::HMAC.hexdigest("SHA256", key, data)
198
+ secure_compare(expected_signature, signature)
199
+ end
200
+ end
201
+
202
+ private
203
+
204
+ def base64url_encode(str)
205
+ Base64.urlsafe_encode64(str).gsub("=", "")
206
+ end
207
+
208
+ def secure_compare(a, b)
209
+ return false unless a.bytesize == b.bytesize
210
+ l = a.unpack "C#{a.bytesize}"
211
+ res = 0
212
+ b.each_byte { |byte| res |= byte ^ l.shift }
213
+ res == 0
150
214
  end
151
215
  end
152
216
  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.1
4
+ version: 0.1.3
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-20 00:00:00.000000000 Z
11
+ date: 2025-12-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis
@@ -122,8 +122,11 @@ executables: []
122
122
  extensions: []
123
123
  extra_rdoc_files: []
124
124
  files:
125
+ - ".DS_Store"
125
126
  - Gemfile
127
+ - Gemfile.lock
126
128
  - README.md
129
+ - Rakefile
127
130
  - lib/reputable.rb
128
131
  - lib/reputable/configuration.rb
129
132
  - lib/reputable/connection.rb