cloudflare-turnstile-rails 0.8.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.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +32 -0
  3. data/Appraisals +65 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +249 -0
  6. data/Rakefile +10 -0
  7. data/lib/cloudflare/turnstile/rails/assets/javascripts/cloudflare_turnstile_helper.js +40 -0
  8. data/lib/cloudflare/turnstile/rails/configuration.rb +41 -0
  9. data/lib/cloudflare/turnstile/rails/constants/cloudflare.rb +19 -0
  10. data/lib/cloudflare/turnstile/rails/constants/error_codes.rb +27 -0
  11. data/lib/cloudflare/turnstile/rails/constants/error_messages.rb +22 -0
  12. data/lib/cloudflare/turnstile/rails/controller_methods.rb +37 -0
  13. data/lib/cloudflare/turnstile/rails/engine.rb +17 -0
  14. data/lib/cloudflare/turnstile/rails/helpers.rb +35 -0
  15. data/lib/cloudflare/turnstile/rails/railtie.rb +22 -0
  16. data/lib/cloudflare/turnstile/rails/verification.rb +80 -0
  17. data/lib/cloudflare/turnstile/rails/version.rb +7 -0
  18. data/lib/cloudflare/turnstile/rails.rb +25 -0
  19. data/lib/generators/cloudflare_turnstile/install_generator.rb +15 -0
  20. data/lib/generators/cloudflare_turnstile/templates/cloudflare_turnstile.rb +18 -0
  21. data/templates/shared/app/controllers/books_controller.rb.tt +42 -0
  22. data/templates/shared/app/controllers/pages_controller.rb +3 -0
  23. data/templates/shared/app/models/book.rb.tt +19 -0
  24. data/templates/shared/app/views/books/_form.html.erb +20 -0
  25. data/templates/shared/app/views/books/create.js.erb +15 -0
  26. data/templates/shared/app/views/books/new.html.erb +5 -0
  27. data/templates/shared/app/views/books/new2.html.erb +9 -0
  28. data/templates/shared/app/views/pages/home.html.erb +4 -0
  29. data/templates/shared/cloudflare_turbolinks_ajax_cache.js +44 -0
  30. data/templates/shared/config/initializers/cloudflare_turnstile.rb +4 -0
  31. data/templates/shared/config/routes.rb +7 -0
  32. data/templates/shared/test/application_system_test_case.rb +5 -0
  33. data/templates/shared/test/controllers/books_controller_test.rb +19 -0
  34. data/templates/shared/test/system/books_test.rb +133 -0
  35. data/templates/template.rb +81 -0
  36. metadata +93 -0
@@ -0,0 +1,80 @@
1
+ require_relative 'constants/cloudflare'
2
+
3
+ module Cloudflare
4
+ module Turnstile
5
+ module Rails
6
+ class VerificationResponse
7
+ attr_reader :raw
8
+
9
+ def initialize(raw)
10
+ @raw = raw
11
+ end
12
+
13
+ def success?
14
+ raw['success'] == true
15
+ end
16
+
17
+ def errors
18
+ raw['error-codes'] || []
19
+ end
20
+
21
+ def action
22
+ raw['action']
23
+ end
24
+
25
+ def cdata
26
+ raw['cdata']
27
+ end
28
+
29
+ def challenge_ts
30
+ raw['challenge_ts']
31
+ end
32
+
33
+ def hostname
34
+ raw['hostname']
35
+ end
36
+
37
+ def metadata
38
+ raw['metadata']
39
+ end
40
+
41
+ def to_h
42
+ raw
43
+ end
44
+ end
45
+
46
+ module Verification
47
+ def self.verify(response:, secret: nil, remoteip: nil, idempotency_key: nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
48
+ raise ConfigurationError, 'Turnstile response token is missing' if response.nil? || response.strip.empty?
49
+
50
+ config = Rails.configuration
51
+ secret ||= config.secret_key
52
+
53
+ raise ConfigurationError, 'Cloudflare Turnstile secret_key is not set.' if secret.nil? || secret.strip.empty?
54
+
55
+ body = {
56
+ 'secret' => secret,
57
+ 'response' => response
58
+ }
59
+ body['remoteip'] = remoteip if remoteip
60
+ body['idempotency_key'] = idempotency_key if idempotency_key
61
+
62
+ uri = URI.parse(Cloudflare::SITE_VERIFY_URL)
63
+ headers = { 'Content-Type' => 'application/x-www-form-urlencoded' }
64
+ http = Net::HTTP.new(uri.host, uri.port)
65
+ http.use_ssl = true
66
+
67
+ request = Net::HTTP::Post.new(uri.request_uri, headers)
68
+ request.set_form_data(body)
69
+
70
+ response = http.request(request)
71
+ json = JSON.parse(response.body)
72
+
73
+ VerificationResponse.new(json)
74
+ rescue JSON::ParserError
75
+ raise ConfigurationError, 'Unable to parse Cloudflare Turnstile verification response'
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,7 @@
1
+ module Cloudflare
2
+ module Turnstile
3
+ module Rails
4
+ VERSION = '0.8.0'.freeze
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ require_relative 'rails/configuration'
2
+ require_relative 'rails/constants/cloudflare'
3
+ require_relative 'rails/engine'
4
+ require_relative 'rails/railtie'
5
+
6
+ module Cloudflare
7
+ module Turnstile
8
+ module Rails
9
+ class Error < StandardError; end
10
+ class ConfigurationError < Error; end
11
+
12
+ def self.configuration
13
+ @configuration ||= Configuration.new
14
+ end
15
+
16
+ def self.configuration=(config)
17
+ @configuration = config
18
+ end
19
+
20
+ def self.configure
21
+ yield(configuration)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module CloudflareTurnstile
4
+ module Generators
5
+ class InstallGenerator < Rails::Generators::Base
6
+ source_root File.expand_path('templates', __dir__)
7
+
8
+ desc 'Creates a Cloudflare Turnstile initializer.'
9
+
10
+ def create_initializer
11
+ copy_file 'cloudflare_turnstile.rb', 'config/initializers/cloudflare_turnstile.rb'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ Cloudflare::Turnstile::Rails.configure do |config|
2
+ # Set your Cloudflare Turnstile Site Key and Secret Key.
3
+ config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', nil)
4
+ config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', nil)
5
+
6
+ # Optional: Customize the script_url to point to a specific Cloudflare Turnstile script URL.
7
+ # By default, the gem uses the standard Cloudflare Turnstile API script.
8
+ # You can override this if you need a custom version of the script or want to add query parameters.
9
+ # config.script_url = "https://challenges.cloudflare.com/turnstile/v0/api.js"
10
+
11
+ # Optional: The render and onload parameters are used to control the behavior of the Turnstile widget.
12
+ # - `render`: Controls the rendering mode of Turnstile (default is 'auto').
13
+ # - `onload`: Defines a callback function name to be called when Turnstile script loads.
14
+ # If you specify `render` or `onload`, the parameters will be appended to the default `script_url`.
15
+ # If `script_url` is provided, it will be used directly and render/onload options will be ignored.
16
+ # config.render = 'explicit'
17
+ # config.onload = 'onloadTurnstileCallback'
18
+ end
@@ -0,0 +1,42 @@
1
+ class BooksController < ApplicationController
2
+ def new
3
+ @book = Book.new
4
+ end
5
+
6
+ def new2
7
+ @book1 = Book.new
8
+ @book2 = Book.new
9
+ end
10
+
11
+ def create
12
+ @book = Book.new(book_params)
13
+
14
+ respond_to do |format|
15
+ if verify_turnstile(model: @book) && @book.valid?
16
+ format.html { redirect_to new_book_url, notice: "Book was successfully created." }
17
+ else
18
+ format.html { render :new, status: :unprocessable_entity }
19
+ format.js
20
+ <% if Gem::Version.new(Rails.version) >= Gem::Version.new("7.0.0") -%>
21
+ format.turbo_stream do
22
+ render turbo_stream: turbo_stream.replace(
23
+ params[:turbo_form_id],
24
+ partial: "books/form",
25
+ locals: { book: @book, turbo_form_id: params[:turbo_form_id] }
26
+ )
27
+ end
28
+ <% end -%>
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def book_params
36
+ <% if Gem::Version.new(Rails.version) >= Gem::Version.new("8.0.0") -%>
37
+ params.expect(book: [:title])
38
+ <% else -%>
39
+ params.require(:book).permit(:title)
40
+ <% end -%>
41
+ end
42
+ end
@@ -0,0 +1,3 @@
1
+ class PagesController < ApplicationController
2
+ def home; end
3
+ end
@@ -0,0 +1,19 @@
1
+ class Book
2
+ include ActiveModel::Model
3
+ include ActiveModel::Validations
4
+ <% if Gem::Version.new(Rails.version) >= Gem::Version.new("6.0.0") -%>
5
+ include ActiveModel::Attributes
6
+
7
+ attribute :title, :string
8
+ <% else -%>
9
+
10
+ attr_accessor :title
11
+
12
+ def initialize(attributes = {})
13
+ super
14
+ @title = attributes[:title]
15
+ end
16
+ <% end -%>
17
+
18
+ validates :title, presence: true
19
+ end
@@ -0,0 +1,20 @@
1
+ <%= form_for(book, remote: true, html: { id: turbo_form_id }) do |form| %>
2
+ <% if book.errors.any? %>
3
+ <ul>
4
+ <% book.errors.full_messages.each do |message| %>
5
+ <li><%= message %></li>
6
+ <% end %>
7
+ </ul>
8
+ <% end %>
9
+
10
+ <%= hidden_field_tag :turbo_form_id, turbo_form_id %>
11
+
12
+ <p>
13
+ <%= form.label :title %>
14
+ <%= form.text_field :title %>
15
+ </p>
16
+
17
+ <%= cloudflare_turnstile_tag data: {theme: 'dark'} %>
18
+
19
+ <%= form.submit %>
20
+ <% end %>
@@ -0,0 +1,15 @@
1
+ (function() {
2
+ const oldForm = document.getElementById("<%= j params[:turbo_form_id] %>");
3
+ const newFormHTML = `<%= j render(partial: 'books/form', locals: { book: @book, turbo_form_id: params[:turbo_form_id] }) %>`;
4
+
5
+ const tempWrapper = document.createElement("div");
6
+ tempWrapper.innerHTML = newFormHTML;
7
+ const newForm = tempWrapper.firstElementChild;
8
+
9
+ oldForm.replaceWith(newForm);
10
+
11
+ const turnstileContainer = newForm.querySelector('.cf-turnstile');
12
+ if (typeof turnstile !== "undefined" && turnstileContainer && turnstileContainer.childElementCount === 0) {
13
+ turnstile.render(turnstileContainer);
14
+ }
15
+ })();
@@ -0,0 +1,5 @@
1
+ <p><%= notice %></p>
2
+
3
+ <h1>New Book</h1>
4
+
5
+ <%= render "form", book: @book, turbo_form_id: 'book-form' %>
@@ -0,0 +1,9 @@
1
+ <p><%= notice %></p>
2
+
3
+ <h1>New Book 1</h1>
4
+
5
+ <%= render "form", book: @book1, turbo_form_id: 'book-form1' %>
6
+
7
+ <h1>New Book 2</h1>
8
+
9
+ <%= render "form", book: @book2, turbo_form_id: 'book-form2' %>
@@ -0,0 +1,4 @@
1
+ <h1>Pages#home</h1>
2
+ <p>Find me in app/views/pages/home.html.erb</p>
3
+
4
+ <%= link_to 'New Book', new_book_path %>
@@ -0,0 +1,44 @@
1
+ /**
2
+ * This script improves UX for Rails 6 applications using Rails UJS + Turbolinks.
3
+ *
4
+ * Problem:
5
+ * When submitting a form remotely (`remote: true`), if the server responds with
6
+ * HTML (e.g., `render :new` or `render :edit` with validation errors),
7
+ * Rails UJS fires `ajax:complete`, but nothing is automatically updated in the DOM.
8
+ *
9
+ * This results in poor UX — users see no feedback when form validation fails.
10
+ *
11
+ * Solution:
12
+ * This listener catches AJAX responses that return full HTML content.
13
+ * If the content type is `text/html`, we:
14
+ * 1. Wrap the response in a Turbolinks snapshot.
15
+ * 2. Cache the snapshot against the current URL.
16
+ * 3. Trigger `Turbolinks.visit()` with `action: 'restore'` to re-render the page.
17
+ *
18
+ * This causes a soft reload (via Turbolinks) that displays the server-rendered
19
+ * form with validation errors — giving users proper feedback without a full reload.
20
+ */
21
+
22
+ document.addEventListener('ajax:complete', event => {
23
+ let referrer, snapshot;
24
+ const xhr = event.detail[0];
25
+
26
+ // Check if the response is HTML (e.g., a rendered form with errors)
27
+ if ((xhr.getResponseHeader('Content-Type') || '').substring(0, 9) === 'text/html') {
28
+ referrer = window.location.href;
29
+
30
+ // Wrap the response in a Turbolinks snapshot
31
+ snapshot = Turbolinks.Snapshot.wrap(xhr.response);
32
+
33
+ // Store the snapshot in Turbolinks' cache
34
+ Turbolinks.controller.cache.put(referrer, snapshot);
35
+
36
+ // Revisit the current page to restore the updated form view with errors
37
+ return Turbolinks.visit(referrer, {
38
+ action: 'restore'
39
+ });
40
+ }
41
+
42
+ // For non-HTML responses (e.g., JSON), do nothing
43
+ return true;
44
+ }, false);
@@ -0,0 +1,4 @@
1
+ Cloudflare::Turnstile::Rails.configure do |config|
2
+ config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', '1x00000000000000000000AA')
3
+ config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA')
4
+ end
@@ -0,0 +1,7 @@
1
+ Rails.application.routes.draw do
2
+ root 'pages#home'
3
+
4
+ resources :books do
5
+ get :new2, on: :collection
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ require 'test_helper'
2
+
3
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
4
+ driven_by :selenium, using: :headless_chrome, screen_size: [1400, 1400]
5
+ end
@@ -0,0 +1,19 @@
1
+ require 'test_helper'
2
+
3
+ class BooksControllerTest < ActionDispatch::IntegrationTest
4
+ test 'throws an exception when secret key is empty' do
5
+ Cloudflare::Turnstile::Rails.configuration.secret_key = nil
6
+
7
+ assert_raises ActionView::Template::Error, 'Cloudflare Turnstile secret_key is not set' do
8
+ get new_book_url
9
+ end
10
+ end
11
+
12
+ test 'throws an exception when site key is nil' do
13
+ Cloudflare::Turnstile::Rails.configuration.site_key = nil
14
+
15
+ assert_raises ActionView::Template::Error, 'Cloudflare Turnstile site_key is not set' do
16
+ get new_book_url
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,133 @@
1
+ require 'application_system_test_case'
2
+
3
+ class BooksTest < ApplicationSystemTestCase
4
+ setup do
5
+ Cloudflare::Turnstile::Rails.configure do |config|
6
+ config.site_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SITE_KEY', '1x00000000000000000000AA')
7
+ config.secret_key = ENV.fetch('CLOUDFLARE_TURNSTILE_SECRET_KEY', '1x0000000000000000000000000000000AA')
8
+ end
9
+ end
10
+
11
+ test 'visiting the from from another page renders turnstile' do
12
+ visit root_url
13
+ click_on 'New Book'
14
+ wait_for_turnstile_inputs(1)
15
+
16
+ assert_selector "div.cf-turnstile input[name='cf-turnstile-response']", count: 1, visible: :all
17
+ end
18
+
19
+ test 'visiting the page twice does not render turnstile twice' do
20
+ visit new_book_url
21
+ wait_for_turnstile_inputs(1)
22
+ visit new_book_url
23
+ wait_for_turnstile_inputs(1)
24
+
25
+ assert_selector "div.cf-turnstile input[name='cf-turnstile-response']", count: 1, visible: :all
26
+ end
27
+
28
+ test 'submitting the form with a validation error re-renders turnstile' do
29
+ visit new_book_url
30
+ wait_for_turnstile_inputs(1)
31
+ click_on 'Create Book'
32
+
33
+ assert_text "Title can't be blank"
34
+ wait_for_turnstile_inputs(1)
35
+ end
36
+
37
+ test 'submitting the form with a valid book re-renders turnstile' do
38
+ visit new_book_url
39
+ wait_for_turnstile_inputs(1)
40
+ fill_in 'Title', with: "Wizard's First Rule"
41
+ click_on 'Create Book'
42
+
43
+ assert_text 'Book was successfully created'
44
+ wait_for_turnstile_inputs(1)
45
+ end
46
+
47
+ test 'submitting the form before turnstile is ready shows an error and re-renders turnstile' do
48
+ visit new_book_url
49
+ click_on 'Create Book'
50
+
51
+ assert_text 'Turnstile verification missing.'
52
+ wait_for_turnstile_inputs(1)
53
+ end
54
+
55
+ test 'turnstile does not render when site key is invalid' do
56
+ Cloudflare::Turnstile::Rails.configuration.site_key = 'DUMMY'
57
+ visit new_book_url
58
+
59
+ assert_no_selector(turnstile_selector, visible: :all, wait: 5)
60
+ end
61
+
62
+ test 'turnstile returns an error when secret key is invalid' do
63
+ Cloudflare::Turnstile::Rails.configuration.secret_key = 'DUMMY'
64
+ visit new_book_url
65
+ wait_for_turnstile_inputs(1)
66
+ click_on 'Create Book'
67
+
68
+ assert_text 'Server misconfiguration: Turnstile secret key invalid.'
69
+ wait_for_turnstile_inputs(1)
70
+ end
71
+
72
+ test 'turnstile validation fails when human verification fails' do
73
+ Cloudflare::Turnstile::Rails.configuration.secret_key = '2x0000000000000000000000000000000AA'
74
+ visit new_book_url
75
+ wait_for_turnstile_inputs(1)
76
+ click_on 'Create Book'
77
+
78
+ assert_text 'Turnstile token is invalid.'
79
+ wait_for_turnstile_inputs(1)
80
+ end
81
+
82
+ test 'turnstile validation fails when the token is expired' do
83
+ Cloudflare::Turnstile::Rails.configuration.secret_key = '3x0000000000000000000000000000000AA'
84
+ visit new_book_url
85
+ wait_for_turnstile_inputs(1)
86
+ click_on 'Create Book'
87
+
88
+ assert_text 'Turnstile token has already been used or expired.'
89
+ wait_for_turnstile_inputs(1)
90
+ end
91
+
92
+ test 'turnstile renders two plugins when there are two forms' do
93
+ skip "Not supported in Github actions for Ruby v#{RUBY_VERSION}" if RUBY_VERSION < '2.7.0' && ENV['CI']
94
+
95
+ visit new2_books_url
96
+ wait_for_turnstile_inputs(2)
97
+ all('input[type="submit"]').each { |input| input.click and wait_for_turnstile_inputs(2) }
98
+
99
+ assert_selector 'li', text: "Title can't be blank", count: 2, wait: 5
100
+ end
101
+
102
+ private
103
+
104
+ def turnstile_selector
105
+ "div.cf-turnstile input[name='cf-turnstile-response'][value*='DUMMY']"
106
+ end
107
+
108
+ def wait_for_turnstile_inputs(count, timeout: 5) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
109
+ start = Time.now
110
+ stable_since = nil
111
+
112
+ loop do
113
+ inputs = all("div.cf-turnstile input[name='cf-turnstile-response']", visible: :all)
114
+ size = inputs.size
115
+
116
+ if size == count && inputs.all? { |i| i.value.to_s.strip != '' }
117
+ # once we hit the desired size with nonempty values,
118
+ # wait a moment to make sure it’s stable
119
+ stable_since ||= Time.now
120
+ return if Time.now - stable_since > 0.5
121
+ elsif size > count
122
+ flunk "Expected #{count} Turnstile widgets, but found #{size}"
123
+ else
124
+ # reset the stability countdown if size changed
125
+ stable_since = nil
126
+ end
127
+
128
+ flunk "Timed out waiting for #{count} Turnstile widgets; saw #{size}" if Time.now - start > timeout
129
+
130
+ sleep 0.1
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,81 @@
1
+ # 1) allow `copy_file`/`directory` to find files in templates/shared
2
+ shared = File.expand_path('shared', __dir__)
3
+ source_paths.unshift(shared)
4
+
5
+ # 2) inject our gem under test into the Gemfile
6
+ append_to_file 'Gemfile', <<~RUBY
7
+ gem 'appraisal', require: false
8
+ gem 'minitest-retry', require: false
9
+
10
+ if RUBY_VERSION >= '3.0.0'
11
+ # Include gems that are no longer loaded from standard libraries
12
+ gem 'mutex_m'
13
+ gem 'bigdecimal'
14
+ gem 'drb'
15
+ gem 'benchmark'
16
+ end
17
+
18
+ # Resolve the "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError)" issue
19
+ gem 'concurrent-ruby', '< 1.3.5'
20
+
21
+ #{if Rails::VERSION::STRING < '7.0.0'
22
+ "# Higher versions are unsupported in Rails < 7.0.0\n# gem 'minitest', '< 5.12'"
23
+ end}#{' '}
24
+ #{if Rails::VERSION::STRING < '7.2.0'
25
+ "# Higher versions cause 'uninitialized constant Rack::Handler (NameError)'\ngem 'rack', '< 3.0.0'"
26
+ end}#{' '}
27
+
28
+ if RUBY_VERSION >= '3.1.0'
29
+ # Resolve the "Unknown alias: default (Psych::BadAlias)" error
30
+ gem 'psych', '< 4'
31
+ end
32
+
33
+ #{if Rails::VERSION::STRING.start_with?('5.2.')
34
+ "# Required for Rails 5.2, unsupported in older versions, and deprecated in newer versions\ngem 'webdrivers'"
35
+ end}#{' '}
36
+
37
+ # test against the local checkout of cloudflare-turnstile-rails
38
+ gem 'cloudflare-turnstile-rails', path: "#{File.expand_path('..', __dir__)}"
39
+ RUBY
40
+
41
+ # 3) copy over all the shared app files
42
+ %w[
43
+ app/controllers/pages_controller.rb
44
+ app/controllers/books_controller.rb.tt
45
+ app/models/book.rb.tt
46
+ app/views/pages/home.html.erb
47
+ app/views/books/create.js.erb
48
+ app/views/books/_form.html.erb
49
+ app/views/books/new.html.erb
50
+ app/views/books/new2.html.erb
51
+ config/initializers/cloudflare_turnstile.rb
52
+ config/routes.rb
53
+ test/application_system_test_case.rb
54
+ test/controllers/books_controller_test.rb
55
+ test/system/books_test.rb
56
+ ].each do |shared_path|
57
+ if shared_path.end_with?('.tt')
58
+ template shared_path, shared_path.sub(/\.tt$/, ''), force: true
59
+ else
60
+ copy_file shared_path, shared_path, force: true
61
+ end
62
+ end
63
+
64
+ # 4) configure minitest-retry in test_helper.rb
65
+ gsub_file 'test/test_helper.rb', %r{require ['"]rails/test_help['"]\n},
66
+ "\\0require 'minitest/retry'\nMinitest::Retry.use!\n"
67
+
68
+ # 5) turbo AJAX-cache helper
69
+ packer_js = 'app/javascript/packs/application.js'
70
+ if File.exist?(packer_js)
71
+ copy_file 'cloudflare_turbolinks_ajax_cache.js', 'app/javascript/packs/cloudflare_turbolinks_ajax_cache.js',
72
+ force: true
73
+
74
+ # import it at the very bottom of application.js
75
+ import_line = "\n// restore cached pages on AJAX navigations\n" \
76
+ "import './cloudflare_turbolinks_ajax_cache'\n"
77
+ append_to_file packer_js, import_line
78
+ end
79
+
80
+ # 6) Remove any existing chromedriver-helper gem line from Gemfile (only relevant for Rails 5.x)
81
+ gsub_file 'Gemfile', /^\s*gem ['"]chromedriver-helper['"].*\n/, ''
metadata ADDED
@@ -0,0 +1,93 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloudflare-turnstile-rails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.8.0
5
+ platform: ruby
6
+ authors:
7
+ - Vadim Kononov
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-05-13 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: '5.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '5.0'
26
+ description: Integrates Cloudflare Turnstile into Rails applications, handling script
27
+ injection, CSP-nonce support, and automatic Turbo/Turbolinks reinitialization.
28
+ email:
29
+ - vadim@konoson.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - ".rubocop.yml"
35
+ - Appraisals
36
+ - LICENSE.txt
37
+ - README.md
38
+ - Rakefile
39
+ - lib/cloudflare/turnstile/rails.rb
40
+ - lib/cloudflare/turnstile/rails/assets/javascripts/cloudflare_turnstile_helper.js
41
+ - lib/cloudflare/turnstile/rails/configuration.rb
42
+ - lib/cloudflare/turnstile/rails/constants/cloudflare.rb
43
+ - lib/cloudflare/turnstile/rails/constants/error_codes.rb
44
+ - lib/cloudflare/turnstile/rails/constants/error_messages.rb
45
+ - lib/cloudflare/turnstile/rails/controller_methods.rb
46
+ - lib/cloudflare/turnstile/rails/engine.rb
47
+ - lib/cloudflare/turnstile/rails/helpers.rb
48
+ - lib/cloudflare/turnstile/rails/railtie.rb
49
+ - lib/cloudflare/turnstile/rails/verification.rb
50
+ - lib/cloudflare/turnstile/rails/version.rb
51
+ - lib/generators/cloudflare_turnstile/install_generator.rb
52
+ - lib/generators/cloudflare_turnstile/templates/cloudflare_turnstile.rb
53
+ - templates/shared/app/controllers/books_controller.rb.tt
54
+ - templates/shared/app/controllers/pages_controller.rb
55
+ - templates/shared/app/models/book.rb.tt
56
+ - templates/shared/app/views/books/_form.html.erb
57
+ - templates/shared/app/views/books/create.js.erb
58
+ - templates/shared/app/views/books/new.html.erb
59
+ - templates/shared/app/views/books/new2.html.erb
60
+ - templates/shared/app/views/pages/home.html.erb
61
+ - templates/shared/cloudflare_turbolinks_ajax_cache.js
62
+ - templates/shared/config/initializers/cloudflare_turnstile.rb
63
+ - templates/shared/config/routes.rb
64
+ - templates/shared/test/application_system_test_case.rb
65
+ - templates/shared/test/controllers/books_controller_test.rb
66
+ - templates/shared/test/system/books_test.rb
67
+ - templates/template.rb
68
+ homepage: https://github.com/vkononov/cloudflare-turnstile-rails
69
+ licenses:
70
+ - MIT
71
+ metadata:
72
+ homepage_uri: https://github.com/vkononov/cloudflare-turnstile-rails
73
+ source_code_uri: https://github.com/vkononov/cloudflare-turnstile-rails
74
+ changelog_uri: https://github.com/vkononov/cloudflare-turnstile-rails/releases
75
+ rubygems_mfa_required: 'true'
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: 2.6.0
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubygems_version: 3.6.2
91
+ specification_version: 4
92
+ summary: Simple Cloudflare Turnstile integration for Ruby on Rails.
93
+ test_files: []