eu_captcha 1.0.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ddd0db23dcc5a6704ea068e01f4f35e041cf45411025d8ec42a02fb13e737ef9
4
+ data.tar.gz: d06e2d715767ba0f4f124103552a1d7e09caeb4de5fbe0beae54699203d959dc
5
+ SHA512:
6
+ metadata.gz: 2c41cde2d9f0650caeabe08e722c3a6a50f5c5d2da8ef473fd133a5a0de47ef000ec8a369128972e09332b46d3b07e432fb66fdfee4696cc271cd48bb25cd02f
7
+ data.tar.gz: 83a76af0123ec9d64a6a90a7ba88f54b7becb1127fdf92f3969345445a2ba72c8107f8662cf4c58ca879f3e53206ca782a6c74c8521bf628738901fdbe9e9760
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ BSD 2-Clause License
2
+
3
+ Copyright 2026 Myra Security GmbH
4
+
5
+ Redistribution and use in source and binary forms, with or without modification,
6
+ are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice,
9
+ this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ POSSIBILITY OF SUCH DAMAGE.
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # EU-Captcha Ruby client
2
+
3
+ Privacy-first, no-cookie, no-manual-interaction bot protection for Ruby applications. Automatically filters bots, spam, and credential-stuffing attempts without requiring any user interaction.
4
+
5
+ ## Requirements
6
+
7
+ - Ruby 2.7 or later
8
+ - No runtime dependencies (uses Ruby's built-in `net/http`)
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ gem install eu_captcha
14
+ ```
15
+
16
+ Or add it to your `Gemfile`:
17
+
18
+ ```ruby
19
+ gem 'eu_captcha'
20
+ ```
21
+
22
+ ## Getting credentials
23
+
24
+ 1. Register at [app.eu-captcha.eu](https://app.eu-captcha.eu/user-registration)
25
+ 2. Create a site and copy the **sitekey** and **secret** from the dashboard
26
+
27
+ ## Quick start
28
+
29
+ > **Using a SPA framework?** The script tag and `<div>` approach below is for server-rendered pages.
30
+ > If you are building with React, Vue, or Angular, use the matching npm package for the frontend widget
31
+ > and continue to use this gem for server-side verification only.
32
+ > See [SPA integration guides](https://docs.eu-captcha.eu/integration/spa/) for details.
33
+
34
+ Add the widget script to any page that contains a form you want to protect:
35
+
36
+ ```html
37
+ <script src="https://cdn.eu-captcha.eu/verify.js" async defer></script>
38
+ ```
39
+
40
+ Place the widget inside your form:
41
+
42
+ ```html
43
+ <div class="eu-captcha" data-sitekey="EUCAPTCHA_SITE_KEY"></div>
44
+ ```
45
+
46
+ Verify the submitted token on your server:
47
+
48
+ ```ruby
49
+ require 'eu_captcha'
50
+
51
+ captcha = EuCaptcha::Client.new(
52
+ sitekey: ENV['EUCAPTCHA_SITE_KEY'],
53
+ secret: ENV['EUCAPTCHA_SECRET_KEY']
54
+ )
55
+
56
+ result = captcha.validate(
57
+ token: params['eu-captcha-response'],
58
+ remote_addr: request.ip
59
+ )
60
+
61
+ render_error unless result.success?
62
+ ```
63
+
64
+ ## Configuration options
65
+
66
+ All options are passed as keyword arguments to the constructor.
67
+
68
+ | Option | Type | Default | Description |
69
+ |---------------------|---------|-----------------------|-------------|
70
+ | `sitekey` | String | — | **Required.** Public sitekey from the dashboard. |
71
+ | `secret` | String | — | **Required.** Secret key from the dashboard. Never expose this client-side. |
72
+ | `fail_default` | Boolean | `true` | Return value used for both network and token state when the API cannot be reached. `true` = fail open (allow on error); `false` = fail closed (deny on error). |
73
+ | `check_cdn_headers` | Boolean | `true` | When `true`, the client IP is resolved from CDN/proxy headers (`HTTP_CLIENT_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_X_REAL_IP`) in the Rack environment before falling back to `REMOTE_ADDR`. Set to `false` when not behind a proxy, or when you pass the IP explicitly. |
74
+ | `verify_url` | String | *(production URL)* | Override the EU-Captcha verify endpoint. Useful for testing. |
75
+ | `credentials_url` | String | *(production URL)* | Override the EU-Captcha verify-credentials endpoint. |
76
+
77
+ ## The result object
78
+
79
+ `validate` returns an `EuCaptcha::Result` with these methods:
80
+
81
+ | Method | Returns `true` when… |
82
+ |---------------------|--------------------------------------------------------------|
83
+ | `success?` | The API was reached **and** the token is valid. |
84
+ | `success_network?` | The API call completed without a network or transport error. |
85
+ | `success_token?` | The API reported the submitted token as valid. |
86
+ | `train` | Returns `true`/`false`/`nil` (see below). |
87
+
88
+ Checking both states separately lets you distinguish a user failing the captcha from an API outage:
89
+
90
+ ```ruby
91
+ result = captcha.validate(token: params['eu-captcha-response'], remote_addr: request.ip)
92
+
93
+ unless result.success_network?
94
+ # Could not reach the API — consider logging or alerting
95
+ end
96
+
97
+ unless result.success_token?
98
+ # Token was rejected — the submission is likely automated
99
+ end
100
+ ```
101
+
102
+ The `train` method returns `true` when the API skipped real validation and forced success — typically because the sitekey does not exist, the secret does not match, or the sitekey's protection toggle is disabled. `success?` already accounts for this flag and returns `false` in that case. Returns `nil` on network failure.
103
+
104
+ ## Verifying credentials
105
+
106
+ Use `verify_credentials` to confirm your sitekey and secret are valid without submitting a client token. Useful for startup or configuration checks:
107
+
108
+ ```ruby
109
+ captcha = EuCaptcha::Client.new(
110
+ sitekey: ENV['EUCAPTCHA_SITE_KEY'],
111
+ secret: ENV['EUCAPTCHA_SECRET_KEY']
112
+ )
113
+
114
+ unless captcha.verify_credentials
115
+ # Credentials are invalid or the API is unreachable — log and alert
116
+ end
117
+ ```
118
+
119
+ `verify_credentials` returns `false` on any network or API error rather than raising, so it is safe to call during application initialisation.
120
+
121
+ ## Rails
122
+
123
+ Store credentials in `config/credentials.yml.enc` (or `.env` + dotenv) and create a client in an initializer:
124
+
125
+ **`config/initializers/eu_captcha.rb`**
126
+
127
+ ```ruby
128
+ EU_CAPTCHA = EuCaptcha::Client.new(
129
+ sitekey: Rails.application.credentials.dig(:eucaptcha, :sitekey),
130
+ secret: Rails.application.credentials.dig(:eucaptcha, :secret)
131
+ )
132
+ ```
133
+
134
+ **Controller**
135
+
136
+ ```ruby
137
+ class ContactController < ApplicationController
138
+ def create
139
+ result = EU_CAPTCHA.validate(
140
+ token: params['eu-captcha-response'],
141
+ remote_addr: request.ip,
142
+ user_agent: request.user_agent
143
+ )
144
+
145
+ unless result.success?
146
+ render json: { error: 'CAPTCHA verification failed' }, status: :unprocessable_entity
147
+ return
148
+ end
149
+
150
+ # process the form...
151
+ end
152
+ end
153
+ ```
154
+
155
+ `request.ip` respects Rails' trusted-proxy configuration, so the real visitor IP is forwarded correctly when running behind a CDN or load balancer.
156
+
157
+ ## Sinatra / Rack
158
+
159
+ Pass the Rack environment directly and let the library resolve the client IP automatically:
160
+
161
+ ```ruby
162
+ require 'sinatra'
163
+ require 'eu_captcha'
164
+
165
+ CAPTCHA = EuCaptcha::Client.new(
166
+ sitekey: ENV['EUCAPTCHA_SITE_KEY'],
167
+ secret: ENV['EUCAPTCHA_SECRET_KEY']
168
+ )
169
+
170
+ post '/contact' do
171
+ result = CAPTCHA.validate(
172
+ token: params['eu-captcha-response'],
173
+ rack_env: env
174
+ )
175
+
176
+ halt 422, 'CAPTCHA verification failed' unless result.success?
177
+
178
+ # process the form...
179
+ end
180
+ ```
181
+
182
+ When `rack_env` is provided, `validate` automatically resolves the client IP and User-Agent from the Rack environment, respecting the `check_cdn_headers` setting.
183
+
184
+ ## Further reading
185
+
186
+ - [Full documentation](https://docs.eu-captcha.eu)
187
+ - [Server-side verification reference](https://docs.eu-captcha.eu/integration/server-side-verification/)
188
+ - [SPA integration guides](https://docs.eu-captcha.eu/integration/spa/) (React / Next.js, Vue / Nuxt, Angular)
189
+
190
+ ## License
191
+
192
+ BSD 2-Clause. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Myra Security GmbH
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ require 'net/http'
28
+ require 'json'
29
+ require 'uri'
30
+ require 'ipaddr'
31
+
32
+ module EuCaptcha
33
+ # Server-side EU-Captcha verification client.
34
+ #
35
+ # Instantiate once per application (e.g. as a singleton or dependency-injected
36
+ # service) and call {#validate} on every form submission you want to protect.
37
+ #
38
+ # @example Quick start
39
+ # captcha = EuCaptcha::Client.new(sitekey: ENV['EUCAPTCHA_SITE_KEY'],
40
+ # secret: ENV['EUCAPTCHA_SECRET_KEY'])
41
+ #
42
+ # result = captcha.validate(token: params['eu-captcha-response'],
43
+ # remote_addr: request.ip)
44
+ #
45
+ # render_error unless result.success?
46
+ class Client
47
+ VERIFY_URL = 'https://api.eu-captcha.eu/v1/verify/'
48
+ CREDENTIALS_URL = 'https://api.eu-captcha.eu/v1/verify-credentials'
49
+
50
+ # @param sitekey [String] The public site key used to identify your site on the
51
+ # client side.
52
+ # @param secret [String] The private secret key used to authenticate server-side
53
+ # verification requests. Never expose this client-side.
54
+ # @param verify_url [String] The EU-Captcha verify endpoint. Defaults to the
55
+ # production URL.
56
+ # @param credentials_url [String] The EU-Captcha verify-credentials endpoint. Defaults to
57
+ # the production URL.
58
+ # @param fail_default [Boolean] Whether to treat an API communication failure as a
59
+ # successful validation. Defaults to +true+ to avoid blocking legitimate users when the
60
+ # API is unreachable (fail open). Set to +false+ to deny on error (fail closed).
61
+ # @param check_cdn_headers [Boolean] When +true+ (default), the client IP is resolved from
62
+ # CDN/proxy headers (+HTTP_CLIENT_IP+, +HTTP_X_FORWARDED_FOR+, +HTTP_X_REAL_IP+) in the
63
+ # Rack environment before falling back to +REMOTE_ADDR+. Set to +false+ when running
64
+ # behind no proxy, or when you supply +remote_addr+ explicitly on every {#validate} call.
65
+ #
66
+ # @raise [EuCaptcha::Error] if +sitekey+ or +secret+ are empty.
67
+ def initialize(
68
+ sitekey:,
69
+ secret:,
70
+ verify_url: VERIFY_URL,
71
+ credentials_url: CREDENTIALS_URL,
72
+ fail_default: true,
73
+ check_cdn_headers: true
74
+ )
75
+ raise EuCaptcha::Error, 'sitekey and secret are required' if sitekey.to_s.empty? || secret.to_s.empty?
76
+
77
+ @sitekey = sitekey
78
+ @secret = secret
79
+ @verify_url = verify_url
80
+ @credentials_url = credentials_url
81
+ @fail_default = fail_default
82
+ @check_cdn_headers = check_cdn_headers
83
+ end
84
+
85
+ # Validates a captcha token against the EU-Captcha API.
86
+ #
87
+ # All parameters are optional: pass what you have and let the library fill in the rest
88
+ # from +rack_env+ when provided.
89
+ #
90
+ # On any API or network failure the result reflects +fail_default+ for +state_network+
91
+ # and +state_token+, and +nil+ for +state_train+, so callers can distinguish between a
92
+ # failed validation and a failed network request.
93
+ #
94
+ # @param token [String, nil] The captcha response token submitted by the client.
95
+ # @param remote_addr [String] The client's IP address. When empty, resolved from
96
+ # +rack_env+ (respecting +check_cdn_headers+).
97
+ # @param user_agent [String] The client's User-Agent header. When empty, read from
98
+ # +rack_env['HTTP_USER_AGENT']+.
99
+ # @param rack_env [Hash, nil] A Rack environment hash used for automatic IP and
100
+ # User-Agent resolution when +remote_addr+ or +user_agent+ are not supplied explicitly.
101
+ #
102
+ # @return [EuCaptcha::Result]
103
+ def validate(token: nil, remote_addr: '', user_agent: '', rack_env: nil)
104
+ effective_addr = remote_addr.to_s.empty? ? resolve_client_ip(rack_env) : remote_addr.to_s
105
+ effective_ua = user_agent.to_s.empty? ? rack_user_agent(rack_env) : user_agent.to_s
106
+
107
+ payload = {
108
+ sitekey: @sitekey,
109
+ secret: @secret,
110
+ client_ip: effective_addr,
111
+ client_token: token.to_s,
112
+ client_user_agent: effective_ua
113
+ }
114
+
115
+ body = post_json(@verify_url, payload)
116
+ Result.new(
117
+ state_network: true,
118
+ state_token: body.fetch('success', false) == true,
119
+ state_train: body.fetch('train', false) == true
120
+ )
121
+ rescue StandardError
122
+ Result.new(
123
+ state_network: @fail_default,
124
+ state_token: @fail_default,
125
+ state_train: nil
126
+ )
127
+ end
128
+
129
+ # Checks whether the configured sitekey and secret are valid without requiring a client token.
130
+ #
131
+ # Intended for startup or configuration checks. Returns +false+ on any network or API error
132
+ # rather than raising, so callers can log a warning and continue initialisation.
133
+ #
134
+ # @return [Boolean] +true+ if the API confirms the sitekey/secret pair is valid.
135
+ def verify_credentials
136
+ payload = { sitekey: @sitekey, secret: @secret }
137
+ body = post_json(@credentials_url, payload)
138
+ body.fetch('valid', false) == true
139
+ rescue StandardError
140
+ false
141
+ end
142
+
143
+ private
144
+
145
+ # Resolves the client IP address from a Rack environment hash.
146
+ #
147
+ # When +check_cdn_headers+ is +true+, the following headers are checked in order and the
148
+ # first value that passes IP validation is returned:
149
+ # +HTTP_CLIENT_IP+, +HTTP_X_FORWARDED_FOR+ (first entry), +HTTP_X_REAL_IP+
150
+ # Falls back to +REMOTE_ADDR+ in all cases.
151
+ def resolve_client_ip(rack_env)
152
+ return '' if rack_env.nil?
153
+
154
+ if @check_cdn_headers
155
+ %w[HTTP_CLIENT_IP HTTP_X_FORWARDED_FOR HTTP_X_REAL_IP].each do |header|
156
+ value = rack_env[header].to_s
157
+ next if value.empty?
158
+
159
+ ip = value.split(',').first.to_s.strip
160
+ return ip if valid_ip?(ip)
161
+ end
162
+ end
163
+
164
+ rack_env.fetch('REMOTE_ADDR', '')
165
+ end
166
+
167
+ def rack_user_agent(rack_env)
168
+ rack_env&.fetch('HTTP_USER_AGENT', '') || ''
169
+ end
170
+
171
+ def valid_ip?(ip)
172
+ IPAddr.new(ip)
173
+ true
174
+ rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
175
+ false
176
+ end
177
+
178
+ def post_json(url, payload)
179
+ uri = URI(url)
180
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
181
+ request = Net::HTTP::Post.new(uri)
182
+ request['Content-Type'] = 'application/json'
183
+ request['Accept'] = 'application/json'
184
+ request.body = JSON.generate(payload)
185
+ response = http.request(request)
186
+ JSON.parse(response.body)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Myra Security GmbH
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ module EuCaptcha
28
+ # Raised when the client is misconfigured, for example when +sitekey+ or +secret+ are empty.
29
+ class Error < StandardError; end
30
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Myra Security GmbH
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ module EuCaptcha
28
+ # Holds the outcome of a single {Client#validate} call.
29
+ #
30
+ # The three state values map directly to the API response fields:
31
+ #
32
+ # * +state_network+ – whether the HTTP request completed without a transport error.
33
+ # * +state_token+ – whether the API reported the submitted token as valid
34
+ # (+success+ field in the JSON response).
35
+ # * +state_train+ – the +train+ flag from the API response (+true+/+false+),
36
+ # or +nil+ when no response was received (network failure).
37
+ class Result
38
+ # @param state_network [Boolean] Whether the API request completed successfully.
39
+ # @param state_token [Boolean] Whether the captcha token was accepted as valid by the API.
40
+ # @param state_train [Boolean, nil] The +train+ flag from the API response. +true+ means the
41
+ # API skipped real validation and forced success (misconfigured credentials or disabled
42
+ # protection). +false+ means normal operation. +nil+ when no API response was received
43
+ # (network failure).
44
+ def initialize(state_network:, state_token:, state_train: nil)
45
+ @state_network = state_network
46
+ @state_token = state_token
47
+ @state_train = state_train
48
+ end
49
+
50
+ # Returns +true+ only if the API request succeeded, the token was valid,
51
+ # and the +train+ flag is not set.
52
+ #
53
+ # When +train+ is +true+ the API forced +success+ to +true+ without performing
54
+ # real validation (misconfigured credentials or disabled protection). This
55
+ # method treats that case as a failure so misconfigured sites fail securely
56
+ # by default. Use {#train} to inspect the flag directly.
57
+ #
58
+ # @return [Boolean]
59
+ def success?
60
+ @state_network && @state_token && !@state_train
61
+ end
62
+
63
+ # Returns +true+ if the API request completed without a network or HTTP error.
64
+ #
65
+ # @return [Boolean]
66
+ def success_network?
67
+ @state_network
68
+ end
69
+
70
+ # Returns +true+ if the API confirmed the captcha token as valid.
71
+ #
72
+ # Note: this reflects the raw +success+ field from the API response. When
73
+ # {#train} returns +true+, the API forced this field to +true+ without real
74
+ # validation. Prefer {#success?} for the safe combined check.
75
+ #
76
+ # @return [Boolean]
77
+ def success_token?
78
+ @state_token
79
+ end
80
+
81
+ # Returns the +train+ flag from the API response.
82
+ #
83
+ # +true+ means the API skipped real validation and forced +success+ to +true+ —
84
+ # typically because the sitekey does not exist, the secret does not match,
85
+ # or the sitekey's protection toggle is disabled. In production this means
86
+ # every submission appears successful regardless of whether the user solved
87
+ # the captcha. Check your sitekey and secret immediately if you see this.
88
+ #
89
+ # +false+ means normal operation and {#success_token?} reflects the real result.
90
+ #
91
+ # +nil+ means no API response was received (network failure) so the train
92
+ # state is unknown.
93
+ #
94
+ # @return [Boolean, nil]
95
+ def train
96
+ @state_train
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Myra Security GmbH
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ module EuCaptcha
28
+ VERSION = '1.0.0'
29
+ end
data/lib/eu_captcha.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright 2026 Myra Security GmbH
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without modification,
6
+ # are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice,
9
+ # this list of conditions and the following disclaimer.
10
+ #
11
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ # this list of conditions and the following disclaimer in the documentation
13
+ # and/or other materials provided with the distribution.
14
+ #
15
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
19
+ # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ # POSSIBILITY OF SUCH DAMAGE.
26
+
27
+ require_relative 'eu_captcha/version'
28
+ require_relative 'eu_captcha/error'
29
+ require_relative 'eu_captcha/result'
30
+ require_relative 'eu_captcha/client'
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: eu_captcha
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Myra Security GmbH
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: rspec
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: webmock
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.0'
40
+ description: EU captcha protects your website/API against abuse like form spam and
41
+ credential stuffing
42
+ executables: []
43
+ extensions: []
44
+ extra_rdoc_files: []
45
+ files:
46
+ - LICENSE
47
+ - README.md
48
+ - lib/eu_captcha.rb
49
+ - lib/eu_captcha/client.rb
50
+ - lib/eu_captcha/error.rb
51
+ - lib/eu_captcha/result.rb
52
+ - lib/eu_captcha/version.rb
53
+ homepage: https://eu-captcha.eu
54
+ licenses:
55
+ - BSD-2-Clause
56
+ metadata: {}
57
+ rdoc_options: []
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '2.7'
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubygems_version: 3.6.7
72
+ specification_version: 4
73
+ summary: EU-Captcha server-side verification client for Ruby
74
+ test_files: []