castle-rb 8.0.0 → 9.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.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +317 -0
  3. data/LICENSE +21 -0
  4. data/README.md +290 -112
  5. data/lib/castle/api/filter.rb +3 -1
  6. data/lib/castle/api/list_items/archive.rb +23 -0
  7. data/lib/castle/api/list_items/count.rb +22 -0
  8. data/lib/castle/api/list_items/create.rb +22 -0
  9. data/lib/castle/api/list_items/create_batch.rb +22 -0
  10. data/lib/castle/api/list_items/get.rb +22 -0
  11. data/lib/castle/api/list_items/query.rb +22 -0
  12. data/lib/castle/api/list_items/unarchive.rb +22 -0
  13. data/lib/castle/api/list_items/update.rb +22 -0
  14. data/lib/castle/api/lists/create.rb +23 -0
  15. data/lib/castle/api/lists/delete.rb +22 -0
  16. data/lib/castle/api/lists/get.rb +22 -0
  17. data/lib/castle/api/lists/get_all.rb +22 -0
  18. data/lib/castle/api/lists/query.rb +22 -0
  19. data/lib/castle/api/lists/update.rb +22 -0
  20. data/lib/castle/api/log.rb +2 -1
  21. data/lib/castle/api/privacy/delete_data.rb +23 -0
  22. data/lib/castle/api/privacy/request_data.rb +23 -0
  23. data/lib/castle/api/risk.rb +2 -1
  24. data/lib/castle/client.rb +15 -52
  25. data/lib/castle/client_actions/list_items.rb +49 -0
  26. data/lib/castle/client_actions/lists.rb +39 -0
  27. data/lib/castle/client_actions/privacy.rb +20 -0
  28. data/lib/castle/commands/list_items/archive.rb +24 -0
  29. data/lib/castle/commands/list_items/count.rb +23 -0
  30. data/lib/castle/commands/list_items/create.rb +22 -0
  31. data/lib/castle/commands/list_items/create_batch.rb +22 -0
  32. data/lib/castle/commands/list_items/get.rb +23 -0
  33. data/lib/castle/commands/list_items/query.rb +24 -0
  34. data/lib/castle/commands/list_items/unarchive.rb +23 -0
  35. data/lib/castle/commands/list_items/update.rb +22 -0
  36. data/lib/castle/commands/lists/create.rb +21 -0
  37. data/lib/castle/commands/lists/delete.rb +20 -0
  38. data/lib/castle/commands/lists/get.rb +20 -0
  39. data/lib/castle/commands/lists/get_all.rb +17 -0
  40. data/lib/castle/commands/lists/query.rb +21 -0
  41. data/lib/castle/commands/lists/update.rb +22 -0
  42. data/lib/castle/commands/privacy/delete_data.rb +20 -0
  43. data/lib/castle/commands/privacy/request_data.rb +20 -0
  44. data/lib/castle/core/get_connection.rb +5 -1
  45. data/lib/castle/core/process_response.rb +1 -0
  46. data/lib/castle/core/process_webhook.rb +1 -1
  47. data/lib/castle/core/send_request.rb +2 -1
  48. data/lib/castle/errors.rb +1 -5
  49. data/lib/castle/headers/filter.rb +1 -1
  50. data/lib/castle/version.rb +1 -1
  51. data/lib/castle.rb +36 -17
  52. metadata +62 -162
  53. data/lib/castle/api/approve_device.rb +0 -20
  54. data/lib/castle/api/authenticate.rb +0 -28
  55. data/lib/castle/api/end_impersonation.rb +0 -22
  56. data/lib/castle/api/get_device.rb +0 -20
  57. data/lib/castle/api/get_devices_for_user.rb +0 -20
  58. data/lib/castle/api/report_device.rb +0 -20
  59. data/lib/castle/api/start_impersonation.rb +0 -22
  60. data/lib/castle/api/track.rb +0 -19
  61. data/lib/castle/commands/approve_device.rb +0 -17
  62. data/lib/castle/commands/authenticate.rb +0 -23
  63. data/lib/castle/commands/end_impersonation.rb +0 -25
  64. data/lib/castle/commands/get_device.rb +0 -17
  65. data/lib/castle/commands/get_devices_for_user.rb +0 -17
  66. data/lib/castle/commands/report_device.rb +0 -17
  67. data/lib/castle/commands/start_impersonation.rb +0 -25
  68. data/lib/castle/commands/track.rb +0 -22
  69. data/lib/castle/support/hanami.rb +0 -15
  70. data/lib/castle/support/padrino.rb +0 -19
  71. data/spec/integration/rails/rails_spec.rb +0 -95
  72. data/spec/integration/rails/support/all.rb +0 -6
  73. data/spec/integration/rails/support/application.rb +0 -17
  74. data/spec/integration/rails/support/home_controller.rb +0 -39
  75. data/spec/lib/castle/api/approve_device_spec.rb +0 -17
  76. data/spec/lib/castle/api/authenticate_spec.rb +0 -133
  77. data/spec/lib/castle/api/end_impersonation_spec.rb +0 -59
  78. data/spec/lib/castle/api/filter_spec.rb +0 -5
  79. data/spec/lib/castle/api/get_device_spec.rb +0 -17
  80. data/spec/lib/castle/api/get_devices_for_user_spec.rb +0 -17
  81. data/spec/lib/castle/api/log_spec.rb +0 -5
  82. data/spec/lib/castle/api/report_device_spec.rb +0 -17
  83. data/spec/lib/castle/api/risk_spec.rb +0 -5
  84. data/spec/lib/castle/api/start_impersonation_spec.rb +0 -59
  85. data/spec/lib/castle/api/track_spec.rb +0 -65
  86. data/spec/lib/castle/api_spec.rb +0 -36
  87. data/spec/lib/castle/client_id/extract_spec.rb +0 -47
  88. data/spec/lib/castle/client_spec.rb +0 -337
  89. data/spec/lib/castle/command_spec.rb +0 -9
  90. data/spec/lib/castle/commands/approve_device_spec.rb +0 -24
  91. data/spec/lib/castle/commands/authenticate_spec.rb +0 -86
  92. data/spec/lib/castle/commands/end_impersonation_spec.rb +0 -72
  93. data/spec/lib/castle/commands/filter_spec.rb +0 -72
  94. data/spec/lib/castle/commands/get_device_spec.rb +0 -24
  95. data/spec/lib/castle/commands/get_devices_for_user_spec.rb +0 -24
  96. data/spec/lib/castle/commands/log_spec.rb +0 -73
  97. data/spec/lib/castle/commands/report_device_spec.rb +0 -24
  98. data/spec/lib/castle/commands/risk_spec.rb +0 -73
  99. data/spec/lib/castle/commands/start_impersonation_spec.rb +0 -72
  100. data/spec/lib/castle/commands/track_spec.rb +0 -89
  101. data/spec/lib/castle/configuration_spec.rb +0 -14
  102. data/spec/lib/castle/context/get_default_spec.rb +0 -41
  103. data/spec/lib/castle/context/merge_spec.rb +0 -23
  104. data/spec/lib/castle/context/prepare_spec.rb +0 -42
  105. data/spec/lib/castle/context/sanitize_spec.rb +0 -27
  106. data/spec/lib/castle/core/get_connection_spec.rb +0 -43
  107. data/spec/lib/castle/core/process_response_spec.rb +0 -103
  108. data/spec/lib/castle/core/process_webhook_spec.rb +0 -52
  109. data/spec/lib/castle/core/send_request_spec.rb +0 -97
  110. data/spec/lib/castle/failover/strategy_spec.rb +0 -12
  111. data/spec/lib/castle/headers/extract_spec.rb +0 -103
  112. data/spec/lib/castle/headers/filter_spec.rb +0 -42
  113. data/spec/lib/castle/headers/format_spec.rb +0 -25
  114. data/spec/lib/castle/ips/extract_spec.rb +0 -91
  115. data/spec/lib/castle/logger_spec.rb +0 -39
  116. data/spec/lib/castle/payload/prepare_spec.rb +0 -52
  117. data/spec/lib/castle/secure_mode_spec.rb +0 -7
  118. data/spec/lib/castle/session_spec.rb +0 -61
  119. data/spec/lib/castle/singleton_configuration_spec.rb +0 -14
  120. data/spec/lib/castle/utils/clean_invalid_chars_spec.rb +0 -69
  121. data/spec/lib/castle/utils/clone_spec.rb +0 -19
  122. data/spec/lib/castle/utils/deep_symbolize_keys_spec.rb +0 -50
  123. data/spec/lib/castle/utils/get_timestamp_spec.rb +0 -16
  124. data/spec/lib/castle/utils/merge_spec.rb +0 -13
  125. data/spec/lib/castle/validators/not_supported_spec.rb +0 -19
  126. data/spec/lib/castle/validators/present_spec.rb +0 -25
  127. data/spec/lib/castle/verdict_spec.rb +0 -9
  128. data/spec/lib/castle/version_spec.rb +0 -5
  129. data/spec/lib/castle/webhooks/verify_spec.rb +0 -59
  130. data/spec/lib/castle_spec.rb +0 -58
  131. data/spec/spec_helper.rb +0 -24
  132. data/spec/support/shared_examples/action_request.rb +0 -167
  133. data/spec/support/shared_examples/configuration.rb +0 -99
data/README.md CHANGED
@@ -1,163 +1,341 @@
1
- # Ruby SDK for Castle
1
+ # Castle Ruby SDK
2
2
 
3
- [![Build Status](https://circleci.com/gh/castle/castle-ruby.svg?style=shield&branch=master)](https://circleci.com/gh/castle/castle-ruby)
4
- [![Coverage Status](https://coveralls.io/repos/github/castle/castle-ruby/badge.svg?branch=coveralls)](https://coveralls.io/github/castle/castle-ruby?branch=coveralls)
3
+ [![Specs](https://github.com/castle/castle-ruby/actions/workflows/specs.yml/badge.svg)](https://github.com/castle/castle-ruby/actions/workflows/specs.yml)
4
+ [![Lint](https://github.com/castle/castle-ruby/actions/workflows/lint.yml/badge.svg)](https://github.com/castle/castle-ruby/actions/workflows/lint.yml)
5
5
  [![Gem Version](https://badge.fury.io/rb/castle-rb.svg)](https://badge.fury.io/rb/castle-rb)
6
6
 
7
- **[Castle](https://castle.io) analyzes user behavior in web and mobile apps to stop fraud before it happens.**
7
+ The official Ruby SDK for [Castle](https://castle.io). Castle analyzes user behavior in web and mobile apps to stop fraud before it happens.
8
+
9
+ This gem is a thin, dependency-light wrapper around the [Castle HTTP API](https://reference.castle.io). It exposes:
10
+
11
+ - **Risk Assessment** — `POST /v1/risk`, `POST /v1/filter`
12
+ - **Event logging** — `POST /v1/log` (fire-and-forget, no verdict)
13
+ - **Lists & List Items** — full CRUD + search + batch
14
+ - **Privacy / GDPR** — `POST` and `DELETE /v1/privacy/users` (Article 15 & 17)
15
+ - **Webhook signature verification**
16
+
17
+ A full list of supported events and the JSON shape of every payload is documented at <https://reference.castle.io>.
18
+
19
+ ## Requirements
20
+
21
+ - Ruby `>= 3.2`
22
+ - A [Castle](https://dashboard.castle.io) API secret
8
23
 
9
24
  ## Installation
10
25
 
11
- Add the `castle-rb` gem to your `Gemfile`
26
+ Add the gem to your `Gemfile`:
12
27
 
13
28
  ```ruby
14
29
  gem 'castle-rb'
15
30
  ```
16
31
 
17
- ## Configuration
32
+ Then:
18
33
 
19
- ### Framework configuration
34
+ ```sh
35
+ bundle install
36
+ ```
20
37
 
21
- Load and configure the library with your Castle API secret in an initializer or similar.
38
+ ## Quick start
22
39
 
23
40
  ```ruby
24
- Castle.api_secret = 'YOUR_API_SECRET'
41
+ require 'castle'
42
+
43
+ Castle.api_secret = ENV.fetch('CASTLE_API_SECRET')
44
+
45
+ verdict = Castle::API::Risk.call(
46
+ type: '$login',
47
+ status: '$succeeded',
48
+ request_token: params[:castle_request_token],
49
+ user: { id: '12345', email: 'user@example.com' },
50
+ context: Castle::Context::Prepare.call(request)
51
+ )
52
+
53
+ case verdict[:policy][:action]
54
+ when 'deny' then # block the user
55
+ when 'challenge' then # send 2FA / additional verification
56
+ else # allow
57
+ end
25
58
  ```
26
59
 
27
- A Castle client instance will be made available as `castle` in your
60
+ `Castle::Context::Prepare.call(request)` extracts the IP and the headers Castle needs from a Rack-compatible `request` object. See [Advanced configuration](#advanced-configuration) for how header allow/deny lists and proxy chains are resolved.
61
+
62
+ ## Configuration
28
63
 
29
- - Rails controllers when you add `require 'castle/support/rails'`
64
+ The minimal, recommended setup:
30
65
 
31
- - Padrino controllers when you add `require 'castle/support/padrino'`
66
+ ```ruby
67
+ Castle.configure do |config|
68
+ # Same as `Castle.api_secret = ...`
69
+ config.api_secret = ENV.fetch('CASTLE_API_SECRET')
32
70
 
33
- - Sinatra app when you add `require 'castle/support/sinatra'` (and additionally explicitly add `register Sinatra::Castle` to your `Sinatra::Base` class if you have a modular application)
71
+ # Behavior when Castle's API is unreachable or returns a 5xx.
72
+ # One of: :allow (default), :deny, :challenge, :throw
73
+ config.failover_strategy = :allow
74
+
75
+ # Request timeout in milliseconds (default: 1000).
76
+ # `Castle::RequestError` is raised on timeout.
77
+ config.request_timeout = 1000
78
+ end
79
+ ```
80
+
81
+ ### Logging
34
82
 
35
83
  ```ruby
36
- require 'castle/support/sinatra'
84
+ Castle.configure do |config|
85
+ config.logger = Logger.new($stdout)
86
+ end
87
+ ```
37
88
 
38
- class ApplicationController < Sinatra::Base
39
- register Sinatra::Castle
89
+ The logger only needs to respond to `#info`. Each request and response (with sensitive values stripped) will be logged.
90
+
91
+ ### Multi-environment / multi-tenant
92
+
93
+ Most apps only need one global config, but you can also build standalone `Castle::Configuration` objects and pass them per call:
94
+
95
+ ```ruby
96
+ config = Castle::Configuration.new.tap do |c|
97
+ c.api_secret = ENV.fetch('CASTLE_API_SECRET_TENANT_A')
40
98
  end
99
+
100
+ Castle::API::Risk.call(payload.merge(config: config))
101
+ ```
102
+
103
+ ## Usage
104
+
105
+ All endpoints are exposed as `Castle::API::<Endpoint>.call(payload)` and return a parsed `Hash`. The same payloads can be sent through `Castle::Client` (created from a Rack request), which automatically attaches request context and a do-not-track flag.
106
+
107
+ ### Risk
108
+
109
+ Used for evaluating high-risk events such as logins, registrations, password resets, and transactions. Returns a verdict (`policy[:action]`) plus risk scores and signals.
110
+
111
+ ```ruby
112
+ Castle::API::Risk.call(
113
+ type: '$login',
114
+ status: '$succeeded',
115
+ request_token: params[:castle_request_token],
116
+ user: { id: '12345', email: 'user@example.com' },
117
+ context: Castle::Context::Prepare.call(request)
118
+ )
119
+ ```
120
+
121
+ ### Filter
122
+
123
+ Used to block bots and bad traffic early in the chain (typically registration). Same response shape as Risk.
124
+
125
+ ```ruby
126
+ Castle::API::Filter.call(
127
+ type: '$registration',
128
+ status: '$attempted',
129
+ request_token: params[:castle_request_token],
130
+ params: { email: 'user@example.com' },
131
+ context: Castle::Context::Prepare.call(request)
132
+ )
133
+ ```
134
+
135
+ ### Log
136
+
137
+ Fire-and-forget event logging; no verdict is returned. Useful for events that should be visible in the Castle dashboard but don't need a real-time decision.
138
+
139
+ ```ruby
140
+ Castle::API::Log.call(
141
+ type: '$profile_update',
142
+ status: '$succeeded',
143
+ user: { id: '12345' },
144
+ context: Castle::Context::Prepare.call(request)
145
+ )
146
+ ```
147
+
148
+ ### Lists & List Items
149
+
150
+ Lists let you organize users, IPs, transactions, or any custom property and use them in policies as allow/deny lists. The SDK mirrors the [Lists API](https://reference.castle.io#tag/Lists):
151
+
152
+ ```ruby
153
+ list = Castle::API::Lists::Create.call(
154
+ name: 'Trusted IPs',
155
+ color: 'green',
156
+ primary_field: 'ip.address'
157
+ )
158
+
159
+ Castle::API::ListItems::Create.call(
160
+ list_id: list[:id],
161
+ primary_value: '1.2.3.4'
162
+ )
163
+
164
+ Castle::API::ListItems::Query.call(
165
+ list_id: list[:id],
166
+ filters: { primary_value: '1.2.3.4' }
167
+ )
168
+ ```
169
+
170
+ Available namespaces:
171
+
172
+ - `Castle::API::Lists::{Create, GetAll, Get, Update, Delete, Query}`
173
+ - `Castle::API::ListItems::{Create, CreateBatch, Get, Query, Count, Update, Archive, Unarchive}`
174
+
175
+ `CreateBatch` accepts up to ~1000 items per call and returns processing counts:
176
+
177
+ ```ruby
178
+ Castle::API::ListItems::CreateBatch.call(
179
+ list_id: list[:id],
180
+ items: [
181
+ { primary_value: '1.2.3.4', author: { type: '$other', identifier: 'me' } },
182
+ { primary_value: '5.6.7.8', author: { type: '$other', identifier: 'me' } }
183
+ ]
184
+ )
185
+ # => { total_received: 2, total_processed: 2, created: 2, ... }
41
186
  ```
42
187
 
43
- - Hanami when you add `require 'castle/support/hanami'` and include `Castle::Hanami` to your Hanami application
188
+ ### Privacy (GDPR)
189
+
190
+ To support GDPR Articles 15 (right of access) and 17 (right to be forgotten), the SDK exposes the current `/v1/privacy/users` endpoints. Both take a JSON body with `identifier` and `identifier_type` (`$id` or `$email`):
44
191
 
45
192
  ```ruby
46
- require 'castle/support/hanami'
193
+ Castle::API::Privacy::RequestData.call(
194
+ identifier: 'rhea@example.org',
195
+ identifier_type: '$email'
196
+ )
197
+
198
+ Castle::API::Privacy::DeleteData.call(
199
+ identifier: 'user_42',
200
+ identifier_type: '$id'
201
+ )
202
+ ```
203
+
204
+ For the request flow, Castle compiles the user's data and emails a download link to the privacy address configured in the dashboard. Configure that address before calling `RequestData` for the first time.
47
205
 
48
- module Web
49
- class Application < Hanami::Application
50
- include Castle::Hanami
51
- end
206
+ > The deprecated path-based variants (`POST/DELETE /v1/privacy/users/{id}`) are intentionally not exposed by the SDK.
207
+
208
+ ### Webhook signature verification
209
+
210
+ Castle signs every webhook with `X-Castle-Signature`. Verify it before trusting the payload:
211
+
212
+ ```ruby
213
+ post '/castle/webhooks' do
214
+ Castle::Webhooks::Verify.call(request)
215
+ # signature is valid; proceed
216
+ rescue Castle::WebhookVerificationError
217
+ halt 400
52
218
  end
53
219
  ```
54
220
 
55
- ### Client configuration
221
+ ### Framework helpers
222
+
223
+ Drop-in helpers expose a request-scoped `castle` client:
56
224
 
57
225
  ```ruby
58
- Castle.configure do |config|
59
- # Same as setting it through Castle.api_secret
60
- config.api_secret = 'secret'
61
-
62
- # For authenticate method you can set failover strategies: allow(default), deny, challenge, throw
63
- config.failover_strategy = :deny
64
-
65
- # Castle::RequestError is raised when timing out in milliseconds (default: 1000 milliseconds)
66
- config.request_timeout = 1500
67
-
68
- # Base Castle API url
69
- # config.base_url = "https://api.castle.io/v1"
70
-
71
- # Logger (need to respond to info method) - logs Castle API requests and responses
72
- # config.logger = Logger.new(STDOUT)
73
-
74
- # Allowlisted and Denylisted headers are case insensitive and allow to use _ and - as a separator, http prefixes are removed
75
- # Allowlisted headers
76
- # By default, the SDK sends all HTTP headers, except for Cookie and Authorization.
77
- # If you decide to use a allowlist, the SDK will:
78
- # - always send the User-Agent header
79
- # - send scrubbed values of non-allowlisted headers
80
- # - send proper values of allowlisted headers.
81
- # @example
82
- # config.allowlisted = ['X_HEADER']
83
- # # will send { 'User-Agent' => 'Chrome', 'X_HEADER' => 'proper value', 'Any-Other-Header' => true }
84
- #
85
- # We highly suggest using denylist instead of allowlist, so that Castle can use as many data points
86
- # as possible to secure your users. If you want to use the allowlist, this is the minimal
87
- # amount of headers we recommend:
88
- config.allowlisted = Castle::Configuration::DEFAULT_ALLOWLIST
226
+ require 'castle/support/rails' # `castle` available in controllers
227
+ require 'castle/support/sinatra' # `register Sinatra::Castle` for modular apps
228
+ ```
89
229
 
90
- # Denylisted headers take precedence over allowlisted elements
91
- # We always denylist Cookie and Authentication headers. If you use any other headers that
92
- # might contain sensitive information, you should denylist them.
93
- config.denylisted = ['HTTP-X-header']
94
-
95
- # Castle needs the original IP of the client, not the IP of your proxy or load balancer.
96
- # The SDK will only trust the proxy chain as defined in the configuration.
97
- # We try to fetch the client IP based on X-Forwarded-For or Remote-Addr headers in that order,
98
- # but sometimes the client IP may be stored in a different header or order.
99
- # The SDK can be configured to look for the client IP address in headers that you specify.
100
-
101
- # Sometimes, Cloud providers do not use consistent IP addresses to proxy requests.
102
- # In this case, the client IP is usually preserved in a custom header. Example:
103
- # Cloudflare preserves the client request in the 'Cf-Connecting-Ip' header.
104
- # It would be used like so: config.ip_headers=['Cf-Connecting-Ip']
105
- config.ip_headers = []
106
-
107
- # If the specified header or X-Forwarded-For default contains a proxy chain with public IP addresses,
108
- # then you must choose only one of the following (but not both):
109
- # 1. The trusted_proxies value must match the known proxy IPs. This option is preferable if the IP is static.
110
- # 2. The trusted_proxy_depth value must be set to the number of known trusted proxies in the chain (see below).
111
- # This option is preferable if the IPs are ephemeral, but the depth is consistent.
112
-
113
- # Additionally to make X-Forwarded-For and other headers work better discovering client ip address,
114
- # and not the address of a reverse proxy server, you can define trusted proxies
115
- # which will help to fetch proper ip from those headers
116
-
117
- # In order to extract the client IP of the X-Forwarded-For header
118
- # and not the address of a reverse proxy server, you must define all trusted public proxies
119
- # you can achieve this by listing all the proxies ip defined by string or regular expressions
120
- # in the trusted_proxies setting
121
- config.trusted_proxies = []
122
-
123
- # or by providing number of trusted proxies used in the chain
124
- config.trusted_proxy_depth = 0
125
-
126
- # note that you must pick one approach over the other.
127
-
128
- # If there is no possibility to define options above and there is no other header that holds the client IP,
129
- # then you may set trust_proxy_chain = true to trust all of the proxy IPs in X-Forwarded-For
130
- config.trust_proxy_chain = false
131
- # *Warning*: this mode is highly promiscuous and could lead to wrongly trusting a spoofed IP if the request passes through a malicious proxy
230
+ Each helper memoizes `Castle::Client.from_request(request)` on first access.
132
231
 
133
- # *Note: the default list of proxies that are always marked as "trusted" can be found in: Castle::Configuration::TRUSTED_PROXIES
232
+ For any other framework you can wire it up yourself in one line:
233
+
234
+ ```ruby
235
+ def castle
236
+ @castle ||= Castle::Client.from_request(request)
134
237
  end
135
238
  ```
136
239
 
137
- ### Multi-environment configuration
240
+ ## Advanced configuration
241
+
242
+ The defaults are good for most deployments. The options below only matter if you have a non-trivial proxy chain or strict header policies.
138
243
 
139
- It is also possible to define multiple configs within one application.
244
+ ### Header allow/deny lists
245
+
246
+ By default the SDK sends every HTTP header except `Cookie` and `Authorization`. Castle uses these headers to fingerprint the request, so the broader the better.
140
247
 
141
248
  ```ruby
142
- # Initialize new instance of Castle::Configuration
143
- config =
144
- Castle::Configuration.new.tap do |c|
145
- # and set any attribute
146
- c.api_secret = 'YOUR_API_SECRET'
147
- end
249
+ Castle.configure do |config|
250
+ # Always-blocked headers (in addition to Cookie/Authorization).
251
+ config.denylisted = ['HTTP-X-Internal-Header']
252
+
253
+ # Strict allow-list mode. Headers outside the list are sent with
254
+ # scrubbed values, except for User-Agent which is always preserved.
255
+ # We recommend the curated default if you have to use an allow list:
256
+ config.allowlisted = Castle::Configuration::DEFAULT_ALLOWLIST
257
+ end
148
258
  ```
149
259
 
150
- After a successful setup, you can pass the config to any API command as follows:
260
+ Header names are case-insensitive and accept both `_` and `-` as separators. A leading `HTTP_` prefix is stripped automatically.
261
+
262
+ ### Client IP detection
263
+
264
+ Castle needs the original client IP, not the IP of your proxy or load balancer. The SDK reads `X-Forwarded-For` and `Remote-Addr` by default; pick **one** of the strategies below depending on your infrastructure:
151
265
 
152
266
  ```ruby
153
- ::Castle::API::GetDevice.call(device_token: device_token, config: config)
267
+ Castle.configure do |config|
268
+ # 1. Custom header (e.g. Cloudflare's Cf-Connecting-Ip).
269
+ config.ip_headers = ['Cf-Connecting-Ip']
270
+
271
+ # 2. Static, known proxy IPs (strings or regexes).
272
+ config.trusted_proxies = ['10.0.0.1', /\A192\.168\./]
273
+
274
+ # 3. Ephemeral proxies but known chain depth.
275
+ config.trusted_proxy_depth = 2
276
+
277
+ # 4. Last resort: trust the entire X-Forwarded-For chain.
278
+ # Warning: vulnerable to header spoofing if a malicious proxy is in path.
279
+ config.trust_proxy_chain = false
280
+ end
154
281
  ```
155
282
 
156
- ## Usage
283
+ Pick **either** `trusted_proxies` **or** `trusted_proxy_depth`, never both. Private/loopback ranges in `Castle::Configuration::TRUSTED_PROXIES` are always considered trusted.
284
+
285
+ ## Errors
286
+
287
+ All exceptions inherit from `Castle::Error`. The most useful ones:
288
+
289
+ | Class | Raised when |
290
+ | ---------------------------------- | ------------------------------------------------------------- |
291
+ | `Castle::ConfigurationError` | The SDK is misconfigured (missing API secret, bad URL, etc.). |
292
+ | `Castle::RequestError` | Network failure or timeout reaching Castle. |
293
+ | `Castle::InvalidRequestTokenError` | The `request_token` is missing or invalid. |
294
+ | `Castle::InvalidParametersError` | 422 response with validation details. |
295
+ | `Castle::RateLimitError` | 429 response — back off and retry. |
296
+ | `Castle::UnauthorizedError` | 401 — bad API secret. |
297
+ | `Castle::InternalServerError` | 5xx response from Castle. |
298
+ | `Castle::WebhookVerificationError` | Webhook signature did not match. |
299
+
300
+ The full list lives in [`lib/castle/errors.rb`](lib/castle/errors.rb).
157
301
 
158
- See [documentation](https://docs.castle.io/docs/) for how to use this SDK with the Castle APIs
302
+ ## Upgrading to 9.0
303
+
304
+ `9.0` removes a number of legacy endpoints and DSL methods. If you're upgrading from 8.x:
305
+
306
+ | Removed | Replacement |
307
+ | ------------------------------------------------------------------------ | ---------------------------------------- |
308
+ | `Castle::API::Track` / `Castle::Client#track` | `Castle::API::Log` or `Castle::API::Risk` |
309
+ | `Castle::API::Authenticate` / `Castle::Client#authenticate` | `Castle::API::Risk` |
310
+ | `Castle::API::ApproveDevice` / `GetDevice` / `GetDevicesForUser` / `ReportDevice` | No direct replacement — contact support |
311
+ | `Castle::API::StartImpersonation` / `EndImpersonation` | No direct replacement — contact support |
312
+ | `Castle::ImpersonationFailed` | Removed |
313
+
314
+ New in 9.0:
315
+
316
+ - `Castle::API::ListItems::CreateBatch` (`POST /v1/lists/{id}/items/batch`)
317
+ - `Castle::API::Privacy::{RequestData, DeleteData}` (`POST` / `DELETE /v1/privacy/users`) — closes [#261](https://github.com/castle/castle-ruby/issues/261)
318
+ - Failover handlers in `Risk`, `Filter`, and `Log` no longer crash when `options[:user]` is missing — closes [#279](https://github.com/castle/castle-ruby/issues/279)
319
+
320
+ Minimum supported Ruby is now `3.2`. See [`CHANGELOG.md`](CHANGELOG.md) for the full list.
321
+
322
+ ## Contributing
323
+
324
+ Bug reports and pull requests are welcome on [GitHub](https://github.com/castle/castle-ruby).
325
+
326
+ ```sh
327
+ bundle install
328
+ bundle exec rspec # run the test suite
329
+ bin/lint # run RuboCop and Prettier
330
+ ```
331
+
332
+ To test against a specific Rails version, set `BUNDLE_GEMFILE` to one of the files in [`gemfiles/`](gemfiles):
333
+
334
+ ```sh
335
+ BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle install
336
+ BUNDLE_GEMFILE=gemfiles/rails_8.1.gemfile bundle exec rspec
337
+ ```
159
338
 
160
- ## Exceptions
339
+ ## License
161
340
 
162
- `Castle::Error` will be thrown if the Castle API returns a 400 or a 500 level HTTP response.
163
- You can also choose to catch a more [finegrained error](https://github.com/castle/castle-ruby/blob/master/lib/castle/errors.rb).
341
+ The gem is available as open source under the terms of the [MIT License](LICENSE).
@@ -18,7 +18,9 @@ module Castle
18
18
  rescue Castle::RequestError, Castle::InternalServerError => e
19
19
  unless config.failover_strategy == :throw
20
20
  strategy = (config || Castle.config).failover_strategy
21
- return(Castle::Failover::PrepareResponse.new(options[:user][:id], reason: e.to_s, strategy: strategy).call)
21
+ # `user` is optional on /v1/filter (#279) — fall back to `matching_user_id` then nil.
22
+ user_id = options.dig(:user, :id) || options[:matching_user_id]
23
+ return Castle::Failover::PrepareResponse.new(user_id, reason: e.to_s, strategy: strategy).call
22
24
  end
23
25
 
24
26
  raise e
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ # Namespace for the list items API ednpoints
6
+ module ListItems
7
+ # Sends DELETE /lists/:list_id/items/:item_id request
8
+ module Archive
9
+ class << self
10
+ # @param options [Hash]
11
+ # @return [Hash]
12
+ def call(options = {})
13
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
14
+ http = options.delete(:http)
15
+ config = options.delete(:config) || Castle.config
16
+
17
+ Castle::API.call(Castle::Commands::ListItems::Archive.build(options), {}, http, config)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends POST /lists/:list_id/items request
7
+ module Count
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Count.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends POST /lists/:list_id/items request
7
+ module Create
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Create.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends POST /lists/{list_id}/items/batch request
7
+ module CreateBatch
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::CreateBatch.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends GET /lists/:list_id/items/:item_id request
7
+ module Get
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Get.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends POST /lists/:list_id/items/query request
7
+ module Query
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Query.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends PUT /lists/:list_id/items/:item_id/unarchive request
7
+ module Unarchive
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Unarchive.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Castle
4
+ module API
5
+ module ListItems
6
+ # Sends PUT /lists/:list_id/items/:item_id request
7
+ module Update
8
+ class << self
9
+ # @param options [Hash]
10
+ # @return [Hash]
11
+ def call(options = {})
12
+ options = Castle::Utils::DeepSymbolizeKeys.call(options || {})
13
+ http = options.delete(:http)
14
+ config = options.delete(:config) || Castle.config
15
+
16
+ Castle::API.call(Castle::Commands::ListItems::Update.build(options), {}, http, config)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end