verify_human 0.1.5
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/app/assets/javascripts/verify_human/turnstile_controller.js +90 -0
- data/app/controllers/concerns/human_verification.rb +69 -0
- data/app/helpers/human_verification_helper.rb +28 -0
- data/config/importmap.rb +1 -0
- data/lib/verify_human/engine.rb +21 -0
- data/lib/verify_human/test_helpers.rb +44 -0
- data/lib/verify_human/version.rb +3 -0
- data/lib/verify_human.rb +5 -0
- metadata +80 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0b8f03bb8489d979c45ce89d32a7060cddad065f4e10519c3e66a83349855076
|
|
4
|
+
data.tar.gz: aab710a58a80595c172ecf3587e398e7ea3295570e052b8fc68a2ff11d5cd163
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 48d7bdce8bd369f27077241dcb8fee2118cae1575ea1fb38de40e5b102da1d8984c83ab86e8e7f96209255370b69871f43cade994e5ad219945e6fea005baf38
|
|
7
|
+
data.tar.gz: 5634039ac132fc72b2e57185a259e1159f496527a09661699133b2f118270a4dd8a1188949166a7c3a4f78e309f820cd06f2cee617855765ab6cefeae582f935
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Marc Köhlbrugge
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# verify_human
|
|
2
|
+
|
|
3
|
+
Human verification for Rails forms. Cloudflare Turnstile under the hood; provider-agnostic API on the surface.
|
|
4
|
+
|
|
5
|
+
This is an opinionated gem: it does human verification the way I like it in my own Rails apps. It's shared in case that way suits you too, not as a general-purpose library that tries to fit every setup.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem "verify_human"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
# ApplicationController (or a specific controller)
|
|
17
|
+
include HumanVerification
|
|
18
|
+
|
|
19
|
+
# Any controller with a form bots abuse. Guards :create by default;
|
|
20
|
+
# declare AFTER rate_limit so the cheap local check runs first.
|
|
21
|
+
rate_limit to: 5, within: 15.minutes, only: :create
|
|
22
|
+
verify_human "sign_in"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```erb
|
|
26
|
+
<%# In the form. Invisible for ~98% of visitors (interaction-only Managed
|
|
27
|
+
widget); shows a one-click checkbox only when Cloudflare requires it. %>
|
|
28
|
+
<%= human_verification_tag "sign_in" %>
|
|
29
|
+
|
|
30
|
+
<%# Submit the surrounding form as soon as a token is issued: %>
|
|
31
|
+
<%= human_verification_tag "apply", auto_submit: true %>
|
|
32
|
+
|
|
33
|
+
<%# Never show UI at all (separate invisible-mode widget; visitors who can't
|
|
34
|
+
pass silently post tokenless -- pair with a retry/interstitial flow): %>
|
|
35
|
+
<%= human_verification_tag "apply", invisible: true %>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Override the failure response per controller (default: `redirect_back` with an alert):
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
def human_verification_failed
|
|
42
|
+
flash.now[:alert] = "We couldn't verify you're human. Please try again."
|
|
43
|
+
render :new, status: :unprocessable_entity # frame-safe, preserves input
|
|
44
|
+
end
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Configuration
|
|
48
|
+
|
|
49
|
+
Reads Rails credentials; verification no-ops unless BOTH keys are present (dev/test are zero-config):
|
|
50
|
+
|
|
51
|
+
```yaml
|
|
52
|
+
turnstile:
|
|
53
|
+
site_key: ...
|
|
54
|
+
secret_key: ...
|
|
55
|
+
invisible_site_key: ... # only if you use invisible: true
|
|
56
|
+
invisible_secret_key: ...
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Create Managed-mode (and optionally invisible-mode) widgets in the Cloudflare dashboard or via API. Development can use Cloudflare's always-pass test keys. Invisible mode requires referencing Cloudflare's Turnstile Privacy Addendum in your privacy policy.
|
|
60
|
+
|
|
61
|
+
## Behavior worth knowing
|
|
62
|
+
|
|
63
|
+
- Fails open on Cloudflare outages (2s/3s timeouts, under a 20s rack-timeout budget); a blank token is rejected without an HTTP call.
|
|
64
|
+
- The siteverify `action` is enforced per form name: a token minted on one widget can't be spent on another endpoint.
|
|
65
|
+
- The Stimulus controller is Turbo-safe: container cleared around Turbo caching (TurnstileError 300030), script-load `onerror` recovery, no render-after-disconnect, and a fresh widget after a Turbo morph (e.g. a form re-rendered with 422 on a page using `turbo_refreshes_with method: :morph`).
|
|
66
|
+
- Users who block challenges.cloudflare.com cannot pass (fail closed by nature of CAPTCHAs) -- keep failure paths friendly.
|
|
67
|
+
|
|
68
|
+
## Tests
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
require "verify_human/test_helpers" # needs WebMock
|
|
72
|
+
|
|
73
|
+
include HumanVerification::TestHelpers
|
|
74
|
+
with_human_verification_configured { ... }
|
|
75
|
+
stub_human_verification(success: true, action: "sign_in")
|
|
76
|
+
stub_human_verification_outage
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Contributing
|
|
80
|
+
|
|
81
|
+
Bug reports are welcome as [GitHub issues](https://github.com/marckohlbrugge/verify_human/issues). I don't accept pull requests: the gem stays as opinionated as it is, so please fork it if you want it to work differently.
|
|
82
|
+
|
|
83
|
+
## Releasing
|
|
84
|
+
|
|
85
|
+
Bump `VerifyHuman::VERSION`, tag `vX.Y.Z`, push the tag. GitHub Actions publishes to rubygems.org as a trusted publisher, so no API key is stored anywhere.
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
// Renders a Cloudflare Turnstile widget. Invisible for most visitors
|
|
4
|
+
// (appearance: interaction-only); only shows a checkbox when Cloudflare
|
|
5
|
+
// requires interaction. With autoSubmit, the surrounding form is submitted
|
|
6
|
+
// as soon as a token is issued.
|
|
7
|
+
let scriptPromise
|
|
8
|
+
|
|
9
|
+
function loadTurnstile() {
|
|
10
|
+
if (window.turnstile) return Promise.resolve()
|
|
11
|
+
|
|
12
|
+
scriptPromise ||= new Promise((resolve) => {
|
|
13
|
+
const script = document.createElement("script")
|
|
14
|
+
script.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit"
|
|
15
|
+
script.async = true
|
|
16
|
+
script.onload = resolve
|
|
17
|
+
script.onerror = () => {
|
|
18
|
+
// Clear the memoized promise so a later connect() can retry, and resolve
|
|
19
|
+
// so awaiting controllers proceed (render is guarded on window.turnstile).
|
|
20
|
+
scriptPromise = undefined
|
|
21
|
+
resolve()
|
|
22
|
+
}
|
|
23
|
+
document.head.appendChild(script)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return scriptPromise
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default class extends Controller {
|
|
30
|
+
static values = { siteKey: String, action: String, autoSubmit: Boolean }
|
|
31
|
+
|
|
32
|
+
connect() {
|
|
33
|
+
this.disconnected = false
|
|
34
|
+
// Turbo must never snapshot a rendered widget: restoring one leaves stale
|
|
35
|
+
// iframe markup in the container, and rendering into a non-empty container
|
|
36
|
+
// crashes the widget (TurnstileError 300030, no token ever minted).
|
|
37
|
+
this.clearBeforeCache = () => this.reset()
|
|
38
|
+
document.addEventListener("turbo:before-cache", this.clearBeforeCache)
|
|
39
|
+
|
|
40
|
+
// A Turbo morph (page refresh, or a form re-rendered with 422 on a page
|
|
41
|
+
// using turbo_refreshes_with :morph) strips the widget's markup but keeps
|
|
42
|
+
// this controller connected, and the token it held is already spent.
|
|
43
|
+
this.renderAfterMorph = (event) => {
|
|
44
|
+
if (event.target === this.element) this.render()
|
|
45
|
+
}
|
|
46
|
+
this.element.addEventListener("turbo:morph-element", this.renderAfterMorph)
|
|
47
|
+
|
|
48
|
+
this.render()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
disconnect() {
|
|
52
|
+
this.disconnected = true
|
|
53
|
+
document.removeEventListener("turbo:before-cache", this.clearBeforeCache)
|
|
54
|
+
this.element.removeEventListener("turbo:morph-element", this.renderAfterMorph)
|
|
55
|
+
this.reset()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async render() {
|
|
59
|
+
await loadTurnstile()
|
|
60
|
+
if (this.disconnected || !window.turnstile) return
|
|
61
|
+
|
|
62
|
+
this.reset() // defense against an already-cached snapshot or a morphed container
|
|
63
|
+
this.widgetId = window.turnstile.render(this.element, {
|
|
64
|
+
sitekey: this.siteKeyValue,
|
|
65
|
+
action: this.actionValue,
|
|
66
|
+
appearance: "interaction-only",
|
|
67
|
+
"response-field-name": "cf_turnstile_response",
|
|
68
|
+
callback: () => this.tokenReceived(),
|
|
69
|
+
// A crashed widget can't mint a token; reset so the form stays usable
|
|
70
|
+
// and the server-side retry flow takes over on submit.
|
|
71
|
+
"error-callback": () => {
|
|
72
|
+
this.reset()
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
tokenReceived() {
|
|
79
|
+
if (this.disconnected) return
|
|
80
|
+
if (this.autoSubmitValue) this.element.closest("form")?.requestSubmit()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reset() {
|
|
84
|
+
if (this.widgetId) {
|
|
85
|
+
window.turnstile.remove(this.widgetId)
|
|
86
|
+
this.widgetId = null
|
|
87
|
+
}
|
|
88
|
+
this.element.replaceChildren()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module HumanVerification
|
|
2
|
+
extend ActiveSupport::Concern
|
|
3
|
+
|
|
4
|
+
VERIFY_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"
|
|
5
|
+
|
|
6
|
+
class_methods do
|
|
7
|
+
# Declare after any rate_limit so the cheap local check runs first.
|
|
8
|
+
# Guards :create by default -- the public form submissions bots abuse.
|
|
9
|
+
# Pass only:/except: to protect other actions (their forms need a widget).
|
|
10
|
+
def verify_human(name = nil, **options)
|
|
11
|
+
options[:only] = :create unless options.key?(:only) || options.key?(:except)
|
|
12
|
+
before_action(**options) { verify_turnstile(name) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def verify_turnstile(expected_name = nil)
|
|
19
|
+
return unless human_verification_enabled?
|
|
20
|
+
return if turnstile_token_valid?(expected_name)
|
|
21
|
+
|
|
22
|
+
human_verification_failed
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Override in controllers that need a custom failure response (e.g. forms
|
|
26
|
+
# inside a turbo frame, where a redirect yields "Content missing" and
|
|
27
|
+
# discards the user's input).
|
|
28
|
+
def human_verification_failed
|
|
29
|
+
redirect_back fallback_location: root_path,
|
|
30
|
+
alert: "We couldn't verify you're human. Please try again."
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Both keys or nothing: enforcing with only a secret_key would reject every
|
|
34
|
+
# submission, because human_verification_tag renders no widget without a site_key.
|
|
35
|
+
# (HumanVerificationHelper duplicates this check for view code.)
|
|
36
|
+
def human_verification_enabled?
|
|
37
|
+
%i[site_key secret_key].all? { |key| Rails.application.credentials.dig(:turnstile, key).present? }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Tokens are widget-specific: a page may run the invisible-mode widget while
|
|
41
|
+
# other forms run the managed one. The form says which one minted the token
|
|
42
|
+
# (spoofing the param just fails verification against the wrong secret).
|
|
43
|
+
def turnstile_secret_key
|
|
44
|
+
key = (params[:cf_turnstile_widget] == "invisible") ? :invisible_secret_key : :secret_key
|
|
45
|
+
Rails.application.credentials.dig(:turnstile, key)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def turnstile_token_valid?(expected_name)
|
|
49
|
+
return false if params[:cf_turnstile_response].blank?
|
|
50
|
+
|
|
51
|
+
response = Faraday.post(VERIFY_URL,
|
|
52
|
+
secret: turnstile_secret_key,
|
|
53
|
+
response: params[:cf_turnstile_response],
|
|
54
|
+
remoteip: request.remote_ip) do |req|
|
|
55
|
+
# Well under rack-timeout's 20s budget, so a slow Cloudflare fails open
|
|
56
|
+
# via the rescue below instead of 500ing the request.
|
|
57
|
+
req.options.open_timeout = 2
|
|
58
|
+
req.options.timeout = 3
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Cloudflare calls the per-widget name "action" in its API. Its testing
|
|
62
|
+
# keys (development) answer without one, flagged in the metadata.
|
|
63
|
+
data = JSON.parse(response.body)
|
|
64
|
+
data["success"] && (expected_name.nil? || data["action"] == expected_name || data.dig("metadata", "result_with_testing_key") == true)
|
|
65
|
+
rescue Faraday::Error, JSON::ParserError => error
|
|
66
|
+
Rails.error.report(error)
|
|
67
|
+
true # Fail open: never lock users out because Cloudflare is unreachable
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module HumanVerificationHelper
|
|
2
|
+
# Renders nothing when no site key is configured (development/test).
|
|
3
|
+
# With auto_submit: true the surrounding form is submitted as soon as
|
|
4
|
+
# Cloudflare issues a token.
|
|
5
|
+
# With invisible: true, an invisible-mode widget runs instead: it never
|
|
6
|
+
# shows UI, and when it cannot pass silently the form simply posts without
|
|
7
|
+
# a token and the app's failure path takes over. The paired hidden field tells
|
|
8
|
+
# the server which widget's secret to verify against.
|
|
9
|
+
def human_verification_tag(name = nil, auto_submit: false, invisible: false, **options)
|
|
10
|
+
return unless human_verification_enabled?
|
|
11
|
+
|
|
12
|
+
widget = tag.div(**options, data: {
|
|
13
|
+
controller: "turnstile",
|
|
14
|
+
turnstile_site_key_value: Rails.application.credentials.dig(:turnstile, invisible ? :invisible_site_key : :site_key),
|
|
15
|
+
turnstile_action_value: name,
|
|
16
|
+
turnstile_auto_submit_value: auto_submit
|
|
17
|
+
})
|
|
18
|
+
invisible ? widget + hidden_field_tag(:cf_turnstile_widget, "invisible") : widget
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Both keys or nothing, matching HumanVerification#human_verification_enabled?
|
|
22
|
+
# (intentionally the same name): rendering a widget without a secret_key
|
|
23
|
+
# would collect tokens the server never checks, and enforcing without a
|
|
24
|
+
# site_key would reject every submission.
|
|
25
|
+
def human_verification_enabled?
|
|
26
|
+
%i[site_key secret_key].all? { |key| Rails.application.credentials.dig(:turnstile, key).present? }
|
|
27
|
+
end
|
|
28
|
+
end
|
data/config/importmap.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pin "controllers/turnstile_controller", to: "verify_human/turnstile_controller.js", preload: false
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module VerifyHuman
|
|
2
|
+
# Non-isolated engine: app/controllers/concerns and app/helpers merge into
|
|
3
|
+
# the host app's autoload and helper paths, so apps keep writing
|
|
4
|
+
# `include HumanVerification` and calling `human_verification_tag` with no
|
|
5
|
+
# namespace changes.
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
initializer "verify_human.assets" do |app|
|
|
8
|
+
if app.config.respond_to?(:assets)
|
|
9
|
+
app.config.assets.paths << root.join("app/assets/javascripts")
|
|
10
|
+
# Sprockets needs an explicit precompile entry; Propshaft ignores this.
|
|
11
|
+
app.config.assets.precompile += %w[verify_human/turnstile_controller.js] if app.config.assets.respond_to?(:precompile)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Pinned under controllers/ so stimulus-loading's lazyLoadControllersFrom
|
|
16
|
+
# discovers and registers the controller exactly like an app-local one.
|
|
17
|
+
initializer "verify_human.importmap", before: "importmap" do |app|
|
|
18
|
+
app.config.importmap.paths << root.join("config/importmap.rb") if defined?(Importmap)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Test support for apps using verify_human. Requires WebMock.
|
|
2
|
+
#
|
|
3
|
+
# require "verify_human/test_helpers"
|
|
4
|
+
# include HumanVerification::TestHelpers
|
|
5
|
+
module HumanVerification
|
|
6
|
+
module TestHelpers
|
|
7
|
+
# Swaps in credentials that answer turnstile keys while delegating every
|
|
8
|
+
# other lookup to the real credentials.
|
|
9
|
+
def with_human_verification_configured(&block)
|
|
10
|
+
credentials = Class.new(SimpleDelegator) do
|
|
11
|
+
def dig(*keys)
|
|
12
|
+
case keys
|
|
13
|
+
in [:turnstile, :secret_key] then "test-secret"
|
|
14
|
+
in [:turnstile, :site_key] then "test-site-key"
|
|
15
|
+
in [:turnstile, :invisible_secret_key] then "test-invisible-secret"
|
|
16
|
+
in [:turnstile, :invisible_site_key] then "test-invisible-site-key"
|
|
17
|
+
else super
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# EncryptedConfiguration claims to respond to everything, which makes
|
|
22
|
+
# Minitest's stub treat this object as a callable and replace it with
|
|
23
|
+
# the result of #call. Deny :call so stub returns the object itself.
|
|
24
|
+
def respond_to?(name, include_private = false)
|
|
25
|
+
return false if name == :call
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
end.new(Rails.application.credentials)
|
|
29
|
+
|
|
30
|
+
Rails.application.stub(:credentials, credentials, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stub_human_verification(success:, action: nil)
|
|
34
|
+
body = {success: success}
|
|
35
|
+
body[:action] = action if action
|
|
36
|
+
stub_request(:post, HumanVerification::VERIFY_URL)
|
|
37
|
+
.to_return(body: body.to_json, headers: {"Content-Type" => "application/json"})
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def stub_human_verification_outage
|
|
41
|
+
stub_request(:post, HumanVerification::VERIFY_URL).to_timeout
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/verify_human.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verify_human
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.5
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Marc Köhlbrugge
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '7.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '7.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
description: verify_human class macro, human_verification_tag view helper, and a Turbo-safe
|
|
41
|
+
Stimulus controller. Interaction-only Managed widgets by default, optional invisible-mode
|
|
42
|
+
widgets, fail-open verification with tight timeouts, per-form action enforcement.
|
|
43
|
+
executables: []
|
|
44
|
+
extensions: []
|
|
45
|
+
extra_rdoc_files: []
|
|
46
|
+
files:
|
|
47
|
+
- LICENSE
|
|
48
|
+
- README.md
|
|
49
|
+
- app/assets/javascripts/verify_human/turnstile_controller.js
|
|
50
|
+
- app/controllers/concerns/human_verification.rb
|
|
51
|
+
- app/helpers/human_verification_helper.rb
|
|
52
|
+
- config/importmap.rb
|
|
53
|
+
- lib/verify_human.rb
|
|
54
|
+
- lib/verify_human/engine.rb
|
|
55
|
+
- lib/verify_human/test_helpers.rb
|
|
56
|
+
- lib/verify_human/version.rb
|
|
57
|
+
homepage: https://github.com/marckohlbrugge/verify_human
|
|
58
|
+
licenses:
|
|
59
|
+
- MIT
|
|
60
|
+
metadata:
|
|
61
|
+
source_code_uri: https://github.com/marckohlbrugge/verify_human
|
|
62
|
+
bug_tracker_uri: https://github.com/marckohlbrugge/verify_human/issues
|
|
63
|
+
rdoc_options: []
|
|
64
|
+
require_paths:
|
|
65
|
+
- lib
|
|
66
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - ">="
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '3.2'
|
|
71
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
requirements: []
|
|
77
|
+
rubygems_version: 3.6.9
|
|
78
|
+
specification_version: 4
|
|
79
|
+
summary: Human verification for Rails forms; Cloudflare Turnstile under the hood
|
|
80
|
+
test_files: []
|