turnstile-ruby 0.1.0 → 0.2.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.
@@ -1,139 +1,146 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "erb"
4
+
3
5
  module Turnstile
4
6
  module Helpers
5
7
  DEFAULT_ERRORS = {
6
- turnstile_unreachable: 'Oops, we failed to validate your Turnstile response. Please try again.',
7
- verification_failed: 'Turnstile verification failed, please try again.'
8
+ turnstile_unreachable: "Oops, we failed to validate your Turnstile response. Please try again.",
9
+ verification_failed: "Turnstile verification failed, please try again."
8
10
  }.freeze
9
11
 
12
+ # Options rendered as data-* attributes on the widget, see
13
+ # https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/widget-configurations/
14
+ WIDGET_OPTIONS = %i[
15
+ action cdata callback error_callback expired_callback timeout_callback before_interactive_callback
16
+ after_interactive_callback unsupported_callback theme language tabindex response_field response_field_name
17
+ size retry retry_interval refresh_expired refresh_timeout appearance execution feedback_enabled
18
+ offlabel_show_privacy offlabel_show_help
19
+ ].freeze
20
+
21
+ # The only widget options where +false+ is a value, everywhere else it means "leave it out".
22
+ BOOLEAN_OPTIONS = %i[response_field feedback_enabled offlabel_show_privacy offlabel_show_help].freeze
23
+
24
+ # Options that control the helper itself and never end up as HTML attributes.
25
+ SETTINGS = %i[
26
+ env site_key sitekey hl onload render script_async script_defer script external_script inline_script
27
+ nonce turbo ui text
28
+ ].freeze
29
+
30
+ HELPER_SCRIPT = File.read(File.expand_path("helper.js", __dir__)).gsub(%r{^\s*//.*\n}, "").gsub(/^\s+/, "").freeze
31
+
10
32
  def self.to_error_message(key)
11
- default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError "Unknown Turnstile captcha error - #{key}" }
33
+ default = DEFAULT_ERRORS.fetch(key) { raise ArgumentError, "Unknown Turnstile captcha error - #{key}" }
12
34
  to_message("turnstile.errors.#{key}", default)
13
35
  end
14
36
 
15
- if defined?(I18n)
16
- def self.to_message(key, default)
17
- I18n.translate(key, default: default)
18
- end
19
- else
20
- def self.to_message(_key, default)
21
- default
22
- end
37
+ def self.to_message(key, default)
38
+ defined?(I18n) ? I18n.translate(key, default: default) : default
23
39
  end
24
40
 
25
- def self.turnstile_tags(options)
41
+ def self.turnstile_tags(options = {})
26
42
  if options.key?(:ssl)
27
43
  raise(TurnstileError, "SSL is now always true. Please remove 'ssl' from your calls to turnstile_tags.")
28
44
  end
29
45
 
30
- html, tag_attributes = components(options.dup)
31
- html << %(<div #{tag_attributes}></div>\n)
46
+ options = options.dup
47
+ settings = extract(options, SETTINGS)
48
+ attributes = data_attributes(options, settings)
49
+ attributes["class"] = ["cf-turnstile", options.delete(:class)].compact.join(" ")
32
50
 
33
- html.respond_to?(:html_safe) ? html.html_safe : html
51
+ html = script_tags(settings, helper: turbo?(settings) && settings[:render].to_s != "explicit")
52
+ html << %(<div #{tag_attributes(attributes.merge(options))}></div>\n)
53
+ safe(html)
54
+ end
55
+
56
+ # Renders a submit button that runs the challenge when the form is submitted and only then submits it.
57
+ def self.invisible_turnstile_tags(custom = {})
58
+ options = { ui: :button, appearance: "interaction-only" }.merge(custom)
59
+ settings = extract(options, SETTINGS)
60
+ tabindex = options.delete(:tabindex)
61
+ widget = data_attributes(options, settings).merge("data-turnstile-invisible" => true)
62
+ options[:tabindex] = tabindex if tabindex
63
+
64
+ html = script_tags(settings, helper: true)
65
+ html << %(<div #{tag_attributes(widget)}></div>\n)
66
+ html << submit_tag(settings, options)
67
+ safe(html)
34
68
  end
35
69
 
36
- def self.invisible_turnstile_tags(custom)
37
- options = { callback: 'javascriptCallback', ui: :button }.merge(custom)
38
- text = options.delete(:text)
39
- html, tag_attributes = components(options.dup)
40
- html << default_callback(options) if default_callback_required?(options)
70
+ private_class_method def self.submit_tag(settings, attributes)
71
+ text = escape(settings[:text])
41
72
 
42
- case options[:ui]
73
+ case settings[:ui]
43
74
  when :button
44
- html << %(<button type="submit" #{tag_attributes}>#{text}</button>\n)
75
+ %(<button #{tag_attributes({ "type" => "submit" }.merge(attributes))}>#{text}</button>\n)
45
76
  when :input
46
- html << %(<input type="submit" #{tag_attributes} value="#{text}"/>\n)
77
+ %(<input #{tag_attributes({ "type" => "submit" }.merge(attributes))} value="#{text}"/>\n)
47
78
  else
48
- raise(TurnstileError, "Turnstile ui `#{options[:ui]}` is not valid.")
79
+ raise(TurnstileError, "Turnstile ui `#{settings[:ui]}` is not valid.")
49
80
  end
50
- html.respond_to?(:html_safe) ? html.html_safe : html
51
81
  end
52
82
 
53
- private_class_method def self.components(options)
54
- html = +''
55
- attributes = {}
83
+ private_class_method def self.data_attributes(options, settings)
84
+ options[:cdata] = options.delete(:cData) if options.key?(:cData)
85
+ options[:language] ||= settings[:hl] if settings[:hl]
86
+ values = extract(options, WIDGET_OPTIONS)
87
+ return {} if Turnstile.skip_env?(settings[:env])
56
88
 
57
- options = options.dup
58
- env = options.delete(:env)
59
- class_attribute = options.delete(:class)
60
- site_key = options.delete(:site_key)
61
- hl = options.delete(:hl)
62
- onload = options.delete(:onload)
63
- render = options.delete(:render)
64
- script_async = options.delete(:script_async)
65
- script_defer = options.delete(:script_defer)
66
- skip_script = (options.delete(:script) == false) || (options.delete(:external_script) == false)
67
- ui = options.delete(:ui)
68
-
69
- data_attribute_keys = %i[sitekey action cData callback expired_callback
70
- timeout_callback error_callback theme language response_field
71
- response_field_name size retry retry_interval refresh_expired]
72
- # data_attribute_keys << :tabindex unless ui == :button # TODO:: Remove me
73
- data_attributes = {}
74
- data_attribute_keys.each do |data_attribute|
75
- value = options.delete(data_attribute)
76
- data_attributes["data-#{data_attribute.to_s.tr('_', '-')}"] = value if value
77
- end
89
+ site_key = settings[:site_key] || settings[:sitekey] || Turnstile.configuration.site_key!
90
+ values.each_with_object("data-sitekey" => site_key) do |(key, value), attributes|
91
+ next if value.nil? || (value == false && !BOOLEAN_OPTIONS.include?(key))
78
92
 
79
- unless Turnstile.skip_env?(env)
80
- site_key ||= Turnstile.configuration.site_key!
81
- script_url = Turnstile.configuration.api_server_url
82
- query_params = hash_to_query(
83
- hl: hl,
84
- onload: onload,
85
- render: render
86
- )
87
- script_url += "?#{query_params}" unless query_params.empty?
88
- async_attr = "async" if script_async != false
89
- defer_attr = "defer" if script_defer != false
90
- html << %(<script src="#{script_url}" #{async_attr} #{defer_attr}></script>\n) unless skip_script
91
- attributes["data-sitekey"] = site_key
92
- attributes.merge! data_attributes
93
+ attributes["data-#{key.to_s.tr("_", "-")}"] = value.to_s
93
94
  end
95
+ end
94
96
 
95
- # The remaining options will be added as attributes on the tag.
96
- attributes["class"] = "cf-turnstile #{class_attribute}"
97
- tag_attributes = attributes.merge(options).map { |k, v| %(#{k}="#{v}") }.join(" ")
97
+ private_class_method def self.script_tags(settings, helper:)
98
+ html = +""
99
+ return html if Turnstile.skip_env?(settings[:env]) || settings[:script] == false
98
100
 
99
- [html, tag_attributes]
101
+ html << api_script_tag(settings) unless settings[:external_script] == false
102
+ html << helper_script_tag(settings) if helper && settings[:inline_script] != false
103
+ html
100
104
  end
101
105
 
102
- private_class_method def self.hash_to_query(hash)
103
- hash.delete_if { |_, val| val.nil? || val.empty? }.to_a.map { |pair| pair.join('=') }.join('&')
106
+ private_class_method def self.api_script_tag(settings)
107
+ query = { onload: settings[:onload], render: settings[:render] }.reject { |_, value| value.to_s.empty? }
108
+ url = Turnstile.configuration.api_server_url
109
+ url += "?#{URI.encode_www_form(query)}" unless query.empty?
110
+
111
+ attributes = { "src" => url }
112
+ attributes["async"] = true unless settings[:script_async] == false
113
+ attributes["defer"] = true unless settings[:script_defer] == false
114
+ attributes["nonce"] = settings[:nonce] || nil
115
+
116
+ %(<script #{tag_attributes(attributes)}></script>\n)
104
117
  end
105
118
 
106
- private_class_method def self.default_callback_required?(options)
107
- options[:callback] == 'javascriptCallback' &&
108
- !Turnstile.skip_env?(options[:env]) &&
109
- options[:script] != false &&
110
- options[:inline_script] != false
119
+ private_class_method def self.helper_script_tag(settings)
120
+ nonce = settings[:nonce] ? %( nonce="#{escape(settings[:nonce])}") : ""
121
+ %(<script#{nonce}>#{HELPER_SCRIPT}</script>\n)
111
122
  end
112
123
 
113
- private_class_method def self.default_callback(options = {})
114
- selector_attr = options[:id] ? "##{options[:id]}" : ".cf-turnstile"
115
-
116
- <<-HTML
117
- <script>
118
- var javascriptCallback = function () {
119
- var closestForm = function (ele) {
120
- var curEle = ele.parentNode;
121
- while (curEle.nodeName !== 'FORM' && curEle.nodeName !== 'BODY'){
122
- curEle = curEle.parentNode;
123
- }
124
- return curEle.nodeName === 'FORM' ? curEle : null
125
- };
126
-
127
- var el = document.querySelector("#{selector_attr}")
128
- if (!!el) {
129
- var form = closestForm(el);
130
- if (form) {
131
- form.submit();
132
- }
133
- }
134
- };
135
- </script>
136
- HTML
124
+ private_class_method def self.turbo?(settings)
125
+ settings.fetch(:turbo) { Turnstile.configuration.turbo } != false
126
+ end
127
+
128
+ private_class_method def self.extract(options, keys)
129
+ keys.each_with_object({}) { |key, found| found[key] = options.delete(key) if options.key?(key) }
130
+ end
131
+
132
+ private_class_method def self.tag_attributes(attributes)
133
+ attributes.reject { |_, value| value.nil? }.map do |name, value|
134
+ value == true ? escape(name) : %(#{escape(name)}="#{escape(value)}")
135
+ end.join(" ")
136
+ end
137
+
138
+ private_class_method def self.escape(value)
139
+ ERB::Util.html_escape(value.to_s)
140
+ end
141
+
142
+ private_class_method def self.safe(html)
143
+ html.respond_to?(:html_safe) ? html.html_safe : html
137
144
  end
138
145
  end
139
146
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # deprecated, but let's not blow everyone up
4
- require 'turnstile'
3
+ # deprecated, but let"s not blow everyone up
4
+ require "turnstile"
@@ -10,7 +10,7 @@ module Turnstile
10
10
  include Turnstile::Adapters::ControllerMethods
11
11
  end
12
12
 
13
- initializer 'turnstile' do |app|
13
+ initializer "turnstile" do |app|
14
14
  Turnstile::Railtie.instance_eval do
15
15
  pattern = pattern_from app.config.i18n.available_locales
16
16
 
@@ -22,13 +22,13 @@ module Turnstile
22
22
  protected
23
23
 
24
24
  def add(pattern)
25
- files = Dir[File.join(File.dirname(__FILE__), '../..', pattern)]
25
+ files = Dir[File.join(File.dirname(__FILE__), "../..", pattern)]
26
26
  I18n.load_path.concat(files)
27
27
  end
28
28
 
29
29
  def pattern_from(args)
30
30
  array = Array(args || [])
31
- array.blank? ? '*' : "{#{array.join ','}}"
31
+ array.blank? ? "*" : "{#{array.join ","}}"
32
32
  end
33
33
  end
34
34
  end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Turnstile
4
+ # The outcome of a token verification, wrapping Cloudflare's siteverify reply.
5
+ #
6
+ # result = Turnstile.verify(token)
7
+ # result.success? # => false
8
+ # result.error_codes # => ["invalid-input-response"]
9
+ # result.hostname # => "example.com"
10
+ #
11
+ class Response
12
+ def initialize(reply, extra_errors: [])
13
+ @reply = reply
14
+ @extra_errors = extra_errors
15
+ end
16
+
17
+ def success?
18
+ @reply["success"].to_s == "true" && @extra_errors.empty?
19
+ end
20
+
21
+ # Cloudflare's error codes, plus "hostname-mismatch" / "action-mismatch" when the
22
+ # reply does not match the hostname or action you asked to validate.
23
+ def error_codes
24
+ Array(@reply["error-codes"]) + @extra_errors
25
+ end
26
+
27
+ def hostname
28
+ @reply["hostname"]
29
+ end
30
+
31
+ def action
32
+ @reply["action"]
33
+ end
34
+
35
+ def cdata
36
+ @reply["cdata"]
37
+ end
38
+
39
+ # ISO 8601 timestamp of the challenge, as sent by Cloudflare.
40
+ def challenge_ts
41
+ @reply["challenge_ts"]
42
+ end
43
+
44
+ def metadata
45
+ @reply["metadata"] || {}
46
+ end
47
+
48
+ # The raw siteverify reply.
49
+ def to_h
50
+ @reply
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "turnstile"
4
+
5
+ module Turnstile
6
+ # Helpers for your own test suite. Not loaded by default:
7
+ #
8
+ # require "turnstile/testing"
9
+ #
10
+ # Turnstile::Testing.failure! # every verification fails
11
+ # Turnstile::Testing.success! # every verification passes
12
+ # Turnstile::Testing.reset! # back to normal
13
+ # Turnstile::Testing.with_failure { post signup_path } # block form, resets afterwards
14
+ #
15
+ # While a stub is active no request is sent to Cloudflare, and `verify_turnstile`
16
+ # uses the stub even in environments listed in `skip_verify_env`.
17
+ module Testing
18
+ module_function
19
+
20
+ def success!(reply = {})
21
+ Turnstile.stubbed_response = Response.new({ "success" => true, "error-codes" => [] }.merge(reply))
22
+ end
23
+
24
+ def failure!(*error_codes)
25
+ error_codes = ["invalid-input-response"] if error_codes.empty?
26
+ Turnstile.stubbed_response = Response.new({ "success" => false, "error-codes" => error_codes.flatten })
27
+ end
28
+
29
+ def reset!
30
+ Turnstile.stubbed_response = nil
31
+ end
32
+
33
+ def with_success(reply = {}, &block)
34
+ with_stub(block) { success!(reply) }
35
+ end
36
+
37
+ def with_failure(*error_codes, &block)
38
+ with_stub(block) { failure!(*error_codes) }
39
+ end
40
+
41
+ def with_stub(block)
42
+ previous = Turnstile.stubbed_response
43
+ yield
44
+ block.call
45
+ ensure
46
+ Turnstile.stubbed_response = previous
47
+ end
48
+ private_class_method :with_stub
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Turnstile
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Lets Bundler auto-require the gem from a plain `gem "turnstile-ruby"` line.
4
+ require "turnstile"
data/lib/turnstile.rb CHANGED
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'json'
4
- require 'net/http'
5
- require 'uri'
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
6
 
7
- require 'turnstile/configuration'
8
- require 'turnstile/helpers'
9
- require 'turnstile/adapters/controller_methods'
10
- require 'turnstile/adapters/view_methods'
7
+ require "turnstile/version"
8
+ require "turnstile/configuration"
9
+ require "turnstile/response"
10
+ require "turnstile/helpers"
11
+ require "turnstile/adapters/controller_methods"
12
+ require "turnstile/adapters/view_methods"
11
13
 
12
- if defined?(Rails)
13
- require 'turnstile/railtie'
14
- end
14
+ require "turnstile/railtie" if defined?(Rails::Railtie)
15
15
 
16
16
  module Turnstile
17
17
  DEFAULT_TIMEOUT = 3
@@ -22,6 +22,16 @@ module Turnstile
22
22
  class VerifyError < TurnstileError
23
23
  end
24
24
 
25
+ # Raised when Cloudflare answers with something that is not a siteverify reply (a 5xx page, an empty body, ...).
26
+ class UnreachableError < TurnstileError
27
+ end
28
+
29
+ class << self
30
+ # A Turnstile::Response returned by every verification instead of calling Cloudflare.
31
+ # Set through Turnstile::Testing (require "turnstile/testing").
32
+ attr_accessor :stubbed_response
33
+ end
34
+
25
35
  # Gives access to the current Configuration.
26
36
  def self.configuration
27
37
  @configuration ||= Configuration.new
@@ -29,10 +39,6 @@ module Turnstile
29
39
 
30
40
  # Allows easy setting of multiple configuration options. See Configuration
31
41
  # for all available options.
32
- #--
33
- # The temp assignment is only used to get a nicer rdoc. Feel free to remove
34
- # this hack.
35
- #++
36
42
  def self.configure
37
43
  config = configuration
38
44
  yield(config)
@@ -51,28 +57,50 @@ module Turnstile
51
57
  original_config.each { |key, value| configuration.send("#{key}=", value) }
52
58
  end
53
59
 
60
+ def self.site_key
61
+ configuration.site_key
62
+ end
63
+
64
+ def self.secret_key
65
+ configuration.secret_key
66
+ end
67
+
54
68
  def self.skip_env?(env)
55
69
  configuration.skip_verify_env.include?(env || configuration.default_env)
56
70
  end
57
71
 
58
72
  def self.invalid_response?(resp)
59
- resp.empty? || resp.length > configuration.response_limit
73
+ !resp.is_a?(String) || resp.empty? || resp.length > configuration.response_limit
60
74
  end
61
75
 
62
- def self.verify_via_api_call(response, options)
63
- secret_key = options.fetch(:secret_key) { configuration.secret_key! }
64
- verify_hash = { 'secret' => secret_key, 'response' => response }
65
- verify_hash['remoteip'] = options[:remote_ip] if options.key?(:remote_ip)
76
+ # Verifies a token with Cloudflare and returns a Turnstile::Response.
77
+ #
78
+ # result = Turnstile.verify(params["cf-turnstile-response"], remote_ip: request.remote_ip)
79
+ # result.success? # => true / false
80
+ # result.error_codes # => ["timeout-or-duplicate"]
81
+ #
82
+ # Options: +secret_key+, +remote_ip+, +idempotency_key+, +timeout+, +hostname+ and +action+.
83
+ # A missing or oversized token fails without a network call. Network timeouts raise Timeout::Error and
84
+ # unusable replies raise Turnstile::UnreachableError.
85
+ def self.verify(token, options = {})
86
+ return stubbed_response if stubbed_response
87
+ return failure(missing_token_code(token)) if invalid_response?(token)
88
+
89
+ reply = api_verification(verify_params(token, options), timeout: options[:timeout])
90
+ errors = []
91
+ errors << "hostname-mismatch" unless hostname_valid?(reply["hostname"], options[:hostname])
92
+ errors << "action-mismatch" unless action_valid?(reply["action"], options[:action])
93
+ Response.new(reply, extra_errors: errors)
94
+ end
66
95
 
67
- reply = api_verification(verify_hash, timeout: options[:timeout])
68
- success = reply['success'].to_s == 'true' &&
69
- hostname_valid?(reply['hostname'], options[:hostname]) &&
70
- action_valid?(reply['action'], options[:action])
96
+ # Kept for backwards compatibility, prefer Turnstile.verify.
97
+ def self.verify_via_api_call(response, options)
98
+ result = verify(response, options)
71
99
 
72
100
  if options[:with_reply] == true
73
- [success, reply]
101
+ [result.success?, result.to_h]
74
102
  else
75
- success
103
+ result.success?
76
104
  end
77
105
  end
78
106
 
@@ -89,12 +117,12 @@ module Turnstile
89
117
  def self.action_valid?(action, expected_action)
90
118
  case expected_action
91
119
  when nil, FalseClass then true
92
- else action == expected_action
120
+ else action == expected_action.to_s
93
121
  end
94
122
  end
95
123
 
96
124
  def self.http_client_for(uri:, timeout: nil)
97
- timeout ||= DEFAULT_TIMEOUT
125
+ timeout ||= configuration.timeout || DEFAULT_TIMEOUT
98
126
  http = if configuration.proxy
99
127
  proxy_server = URI.parse(configuration.proxy)
100
128
  Net::HTTP::Proxy(proxy_server.host, proxy_server.port, proxy_server.user, proxy_server.password)
@@ -110,9 +138,32 @@ module Turnstile
110
138
 
111
139
  def self.api_verification(verify_hash, timeout: nil)
112
140
  uri = URI.parse(configuration.verify_url)
113
- http_instance = http_client_for(uri: uri, timeout: nil)
141
+ http_instance = http_client_for(uri: uri, timeout: timeout)
114
142
  request = Net::HTTP::Post.new(uri.request_uri)
115
143
  request.set_form_data verify_hash
116
- JSON.parse(http_instance.request(request).body)
144
+ reply = JSON.parse(http_instance.request(request).body.to_s)
145
+ raise UnreachableError, "Unexpected reply from Turnstile: #{reply.inspect}" unless reply.is_a?(Hash)
146
+
147
+ reply
148
+ rescue JSON::ParserError
149
+ raise UnreachableError, "Turnstile did not return a JSON reply."
150
+ end
151
+
152
+ def self.verify_params(token, options)
153
+ params = { "secret" => options.fetch(:secret_key) { configuration.secret_key! }, "response" => token }
154
+ params["remoteip"] = options[:remote_ip].to_s if options[:remote_ip]
155
+ params["idempotency_key"] = options[:idempotency_key].to_s if options[:idempotency_key]
156
+ params
157
+ end
158
+ private_class_method :verify_params
159
+
160
+ def self.failure(error_code)
161
+ Response.new({ "success" => false, "error-codes" => [error_code] })
162
+ end
163
+ private_class_method :failure
164
+
165
+ def self.missing_token_code(token)
166
+ token.nil? || token == "" ? "missing-input-response" : "invalid-input-response"
117
167
  end
168
+ private_class_method :missing_token_code
118
169
  end
data/sig/turnstile.rbs ADDED
@@ -0,0 +1,68 @@
1
+ module Turnstile
2
+ VERSION: String
3
+ DEFAULT_TIMEOUT: Integer
4
+
5
+ class TurnstileError < StandardError
6
+ end
7
+
8
+ class VerifyError < TurnstileError
9
+ end
10
+
11
+ class UnreachableError < TurnstileError
12
+ end
13
+
14
+ attr_accessor self.stubbed_response: Response?
15
+
16
+ def self.configuration: () -> Configuration
17
+ def self.configure: () { (Configuration) -> void } -> void
18
+ def self.with_configuration: (Hash[Symbol, untyped] config) ?{ () -> void } -> void
19
+ def self.site_key: () -> String?
20
+ def self.secret_key: () -> String?
21
+ def self.skip_env?: (String? env) -> bool
22
+ def self.verify: (untyped token, ?Hash[Symbol, untyped] options) -> Response
23
+ def self.verify_via_api_call: (untyped response, Hash[Symbol, untyped] options) -> (bool | [bool, Hash[String, untyped]])
24
+
25
+ class Configuration
26
+ attr_accessor default_env: String?
27
+ attr_accessor skip_verify_env: Array[String]
28
+ attr_accessor proxy: String?
29
+ attr_accessor secret_key: String?
30
+ attr_accessor site_key: String?
31
+ attr_accessor handle_timeouts_gracefully: bool
32
+ attr_accessor hostname: (String | ^(String?) -> boolish)?
33
+ attr_accessor response_limit: Integer
34
+ attr_accessor timeout: Numeric
35
+ attr_accessor turbo: bool
36
+ attr_accessor api_server_url: String
37
+ attr_accessor verify_url: String
38
+
39
+ def secret_key!: () -> String
40
+ def site_key!: () -> String
41
+ end
42
+
43
+ class Response
44
+ def initialize: (Hash[String, untyped] reply, ?extra_errors: Array[String]) -> void
45
+ def success?: () -> bool
46
+ def error_codes: () -> Array[String]
47
+ def hostname: () -> String?
48
+ def action: () -> String?
49
+ def cdata: () -> String?
50
+ def challenge_ts: () -> String?
51
+ def metadata: () -> Hash[String, untyped]
52
+ def to_h: () -> Hash[String, untyped]
53
+ end
54
+
55
+ module Helpers
56
+ def self.turnstile_tags: (?Hash[Symbol, untyped] options) -> String
57
+ def self.invisible_turnstile_tags: (?Hash[Symbol, untyped] options) -> String
58
+ def self.to_error_message: (Symbol key) -> String
59
+ end
60
+
61
+ module Testing
62
+ def self.success!: (?Hash[String, untyped] reply) -> Response
63
+ def self.failure!: (*String error_codes) -> Response
64
+ def self.reset!: () -> nil
65
+ def self.with_success: (?Hash[String, untyped] reply) { () -> untyped } -> untyped
66
+ def self.with_failure: (*String error_codes) { () -> untyped } -> untyped
67
+ end
68
+ end