otori 0.4.0 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9139cc5240c7900afc1038f7634429d8cac065d9e22c3645e4891dd9f1fd9103
4
- data.tar.gz: d0adef9a1384d5123b7e304879fb9691ebc35e77e238b39a6348419ac8bde074
3
+ metadata.gz: 554c128771ba6ee67560679dc82411c949a7950188d9d89c50419d4f2135b288
4
+ data.tar.gz: 562127711e5aa8d21bdde1d65cea1209e26f228d50c9b16a75530dcea7248a78
5
5
  SHA512:
6
- metadata.gz: e03586c88e10498d34371303c0d87b09cb8bf643d99e8e30f5b0b19a6ab2851476f6290c547b74d6cfd7c819b281269ccf76d706bc5e42dd03131fa49d4965cf
7
- data.tar.gz: 44b8afe56864c015efc19ee2a00ede9167f53bfe471ec7e74ef5ec5d05414522c2835c173e3c5005248447b2241d15c8b1c20e38ed5d4981d986e6d8456ab823
6
+ metadata.gz: 79b5ba2de508acc8324892a7ee500a2e5d1252bb82ecda1d7c9f809211f12a35d92a2b7d6c64d638a2003a58215ed252495e5b311be5794b97be0eadfd5d02aa
7
+ data.tar.gz: 7ec7f1393886c5064bbec6819b1e6637a5a6366b511485c2d6ae66dfe8cceebc1ec92ca15336427cae491aa496ae4f688585223663a58ec3b372433afb911700
data/CHANGELOG.md CHANGED
@@ -7,6 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - `Otori::Signals#pragmatic_rating` and `Otori.signals_rating(params,
13
+ pragmatic: true)`. The raw `human_rating` is unchanged and stays honest:
14
+ desktop users without a touchscreen can trigger at most four of the five
15
+ signals, capping them at `0.8`. The pragmatic rating treats `0.8` and up
16
+ as fully human without influencing lower ratings: values at or below `0.4`
17
+ are returned untouched, and the `0.4` to `0.8` range is stretched linearly
18
+ to `0.4` to `1.0` (so `0.6` becomes `0.7`). Opt in per call; the default
19
+ `Otori.signals_rating(params)` keeps returning the raw rating.
20
+
21
+ ## [0.5.0] - 2026-10-13
22
+
23
+ ### Added
24
+
25
+ - Passing `wait: false` to `Otori.reason` / `Otori.caught?` (and the
26
+ Rails/Hanami `honeypot` macros) skips the timing check entirely while
27
+ keeping the filled-field check active. Useful for forms where
28
+ password-manager autofill or sticky-session returns make even a short
29
+ timer prone to false positives. `wait: nil` continues to fall back to
30
+ `config.default_delay`; only an explicit `false` disables timing.
31
+
10
32
  ## [0.4.0] - 2026-10-13
11
33
 
12
34
  ### Added
data/README.md CHANGED
@@ -18,7 +18,8 @@ techniques into one gem:
18
18
 
19
19
  1. **Invisible fields**. Bots fill out every field, including ones hidden with CSS.
20
20
  2. **Timing checks**. Bots submit forms instantly, humans need more time.
21
- 3. **Input signals**. Bots don't tend to trigger mouse, touch, scroll, keyboard, or focus events.
21
+ 3. **Input signals**. Bots don't tend to trigger mouse, touch, scroll,
22
+ keyboard, or focus events.
22
23
 
23
24
  When either of the first two checks fail, the submission is quietly rejected.
24
25
  The bot thinks it succeeded and moves on. The third one can be used to reject
@@ -173,6 +174,14 @@ honeypot "note" do |_req, _res|
173
174
  end
174
175
  ```
175
176
 
177
+ Pass `wait: false` to skip the timing check entirely while keeping the
178
+ filled-field check active. Useful on login or password-reset forms where
179
+ password-manager autofill routinely beats any reasonable timer:
180
+
181
+ ```ruby
182
+ honeypot "user[website]", wait: false
183
+ ```
184
+
176
185
  To act on the input-signals rating, evaluate it inside `handle`:
177
186
 
178
187
  ```ruby
@@ -243,6 +252,14 @@ class SignUpsController < ApplicationController
243
252
  end
244
253
  ```
245
254
 
255
+ Pass `wait: false` to skip the timing check entirely while keeping the
256
+ filled-field check active. Useful on forms where password-manager autofill
257
+ beats any reasonable timer (login, password reset):
258
+
259
+ ```ruby
260
+ honeypot :username, wait: false, only: :create
261
+ ```
262
+
246
263
  When the honeypot is tripped, the request halts with `:no_content` by default.
247
264
  To customize the response, pass a block. The block optionally receives the
248
265
  catch reason (`:filled` or `:too_fast`), handy for logging or branching:
@@ -339,6 +356,22 @@ signals.focus?
339
356
  > password manager submissions, which often only trigger focus plus mouse or
340
357
  > touch.
341
358
 
359
+ ### Pragmatic rating
360
+
361
+ Desktop users without a touchscreen can trigger at most four of the five
362
+ signals, capping their raw rating at `0.8`. The raw rating always stays honest,
363
+ but if you want such submissions to count as fully human, ask for the pragmatic
364
+ rating:
365
+
366
+ ```ruby
367
+ Otori.signals_rating(params, pragmatic: true)
368
+ signals.pragmatic_rating
369
+ ```
370
+
371
+ Ratings at or below `0.4` are returned unchanged, the `0.4` to `0.8` range is
372
+ stretched linearly to `0.4` to `1.0`, and anything `0.8` or above counts as
373
+ fully human.
374
+
342
375
  ## Security considerations
343
376
 
344
377
  This gem provides basic bot protection, but it should not be your only line
data/lib/otori/signals.rb CHANGED
@@ -16,7 +16,7 @@ module Otori
16
16
  raw = JSON.parse(json.to_s)
17
17
  raise JSON::ParserError, "expected an object" unless raw.is_a?(Hash)
18
18
 
19
- new(KEYS.transform_values { raw[_1] == true })
19
+ new(KEYS.transform_values { raw[it] == true })
20
20
  rescue JSON::ParserError
21
21
  new
22
22
  end
@@ -25,8 +25,17 @@ module Otori
25
25
  from_json(json).human_rating
26
26
  end
27
27
 
28
+ def self.pragmatic_rating(json)
29
+ from_json(json).pragmatic_rating
30
+ end
31
+
32
+ # Touchless devices can trigger at most four signals, capping them at 0.8,
33
+ # so this stretches 0.4-0.8 to 0.4-1.0 to let them rate as fully human.
34
+ PRAGMATIC_ANCHOR = 0.4
35
+ PRAGMATIC_CLEAN = 0.8
36
+
28
37
  def initialize(flags = {})
29
- @flags = KEYS.keys.to_h { [_1, flags.fetch(_1, false) == true] }
38
+ @flags = KEYS.keys.to_h { [it, flags.fetch(it, false) == true] }
30
39
  end
31
40
 
32
41
  KEYS.each_key do |key|
@@ -37,6 +46,15 @@ module Otori
37
46
  @flags.values.count(true) / KEYS.size.to_f
38
47
  end
39
48
 
49
+ def pragmatic_rating
50
+ raw = human_rating
51
+ return raw if raw <= PRAGMATIC_ANCHOR
52
+ return 1.0 if raw >= PRAGMATIC_CLEAN
53
+
54
+ span = (1.0 - PRAGMATIC_ANCHOR) / (PRAGMATIC_CLEAN - PRAGMATIC_ANCHOR)
55
+ (PRAGMATIC_ANCHOR + (raw - PRAGMATIC_ANCHOR) * span).round(2)
56
+ end
57
+
40
58
  def to_h = @flags.dup
41
59
  end
42
60
  end
@@ -10,6 +10,7 @@ module Otori
10
10
 
11
11
  def elapsed?(timestamp_ms, wait_seconds, now: monotonic_ms)
12
12
  return true if Otori.config.disable_delay
13
+ return true if wait_seconds == false
13
14
  return false if timestamp_ms.nil?
14
15
 
15
16
  (now - timestamp_ms.to_i) >= (wait_seconds.to_f * 1000)
data/lib/otori/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Otori
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/otori.rb CHANGED
@@ -35,7 +35,7 @@ module Otori
35
35
  end
36
36
 
37
37
  def reason(name, params:, session:, wait: nil)
38
- wait ||= config.default_delay
38
+ wait = config.default_delay if wait.nil?
39
39
  session_key = config.session_key(name)
40
40
  stored = session_get(session, session_key)
41
41
  filled = Validator.filled?(param_value(params, name))
@@ -50,11 +50,15 @@ module Otori
50
50
  caught
51
51
  end
52
52
 
53
- def signals_rating(params)
53
+ def signals_rating(params, pragmatic: false)
54
54
  raw = param_value(params, config.signals_input_name)
55
55
  return 0.0 if raw.nil? || raw.to_s.empty?
56
56
 
57
- Signals.human_rating(raw.to_s)
57
+ if pragmatic
58
+ Signals.pragmatic_rating(raw.to_s)
59
+ else
60
+ Signals.human_rating(raw.to_s)
61
+ end
58
62
  end
59
63
 
60
64
  private
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: otori
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wout