vpndetection 5.1.0 → 5.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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +30 -3
  3. data/lib/vpndetection/api/account_wire_api.rb +1 -1
  4. data/lib/vpndetection/api/api_keys_wire_api.rb +1 -1
  5. data/lib/vpndetection/api/authorization_wire_api.rb +19 -17
  6. data/lib/vpndetection/api/database_wire_api.rb +7 -3
  7. data/lib/vpndetection/api/entitlement_wire_api.rb +1 -1
  8. data/lib/vpndetection/api/lookup_wire_api.rb +1 -1
  9. data/lib/vpndetection/api/organization_wire_api.rb +1 -1
  10. data/lib/vpndetection/api_client.rb +1 -1
  11. data/lib/vpndetection/api_error.rb +1 -1
  12. data/lib/vpndetection/api_model_base.rb +1 -1
  13. data/lib/vpndetection/client.rb +29 -13
  14. data/lib/vpndetection/configuration.rb +1 -1
  15. data/lib/vpndetection/database_api.rb +18 -10
  16. data/lib/vpndetection/errors.rb +54 -13
  17. data/lib/vpndetection/models/account_create_apikey_request.rb +1 -1
  18. data/lib/vpndetection/models/account_created_apikey.rb +1 -1
  19. data/lib/vpndetection/models/account_org.rb +1 -1
  20. data/lib/vpndetection/models/account_org_ref.rb +1 -1
  21. data/lib/vpndetection/models/account_org_wrap.rb +1 -1
  22. data/lib/vpndetection/models/account_rc.rb +1 -1
  23. data/lib/vpndetection/models/account_revealed_apikey.rb +1 -1
  24. data/lib/vpndetection/models/account_user.rb +1 -1
  25. data/lib/vpndetection/models/apikey_detail.rb +1 -1
  26. data/lib/vpndetection/models/apikey_list.rb +1 -1
  27. data/lib/vpndetection/models/batch_lookup_error.rb +1 -1
  28. data/lib/vpndetection/models/batch_lookup_request.rb +1 -1
  29. data/lib/vpndetection/models/batch_lookup_response.rb +1 -1
  30. data/lib/vpndetection/models/class_detail.rb +1 -1
  31. data/lib/vpndetection/models/database.rb +1 -1
  32. data/lib/vpndetection/models/database_checksums_response.rb +1 -1
  33. data/lib/vpndetection/models/database_format.rb +1 -1
  34. data/lib/vpndetection/models/database_format_size.rb +1 -1
  35. data/lib/vpndetection/models/database_list.rb +1 -1
  36. data/lib/vpndetection/models/database_metadata.rb +1 -1
  37. data/lib/vpndetection/models/database_metadata_column.rb +1 -1
  38. data/lib/vpndetection/models/database_version.rb +1 -1
  39. data/lib/vpndetection/models/db_checksums.rb +1 -1
  40. data/lib/vpndetection/models/device_authorization.rb +2 -1
  41. data/lib/vpndetection/models/download.rb +1 -1
  42. data/lib/vpndetection/models/download_list.rb +1 -1
  43. data/lib/vpndetection/models/entitlement.rb +1 -1
  44. data/lib/vpndetection/models/entitlement_apikey.rb +1 -1
  45. data/lib/vpndetection/models/entitlement_error.rb +1 -1
  46. data/lib/vpndetection/models/entitlement_plan.rb +1 -1
  47. data/lib/vpndetection/models/entitlement_usage.rb +1 -1
  48. data/lib/vpndetection/models/error_envelope.rb +1 -1
  49. data/lib/vpndetection/models/identity.rb +1 -1
  50. data/lib/vpndetection/models/lookup_error.rb +1 -1
  51. data/lib/vpndetection/models/lookup_response.rb +1 -1
  52. data/lib/vpndetection/models/oauth_error.rb +1 -1
  53. data/lib/vpndetection/models/oauth_metadata.rb +30 -9
  54. data/lib/vpndetection/models/proxy_detail.rb +1 -1
  55. data/lib/vpndetection/models/standing.rb +1 -1
  56. data/lib/vpndetection/models/token_response.rb +28 -5
  57. data/lib/vpndetection/models/vpn_detail.rb +1 -1
  58. data/lib/vpndetection/oauth_api.rb +166 -0
  59. data/lib/vpndetection/retries.rb +4 -2
  60. data/lib/vpndetection/transport.rb +58 -5
  61. data/lib/vpndetection/version.rb +1 -1
  62. data/lib/vpndetection.rb +1 -0
  63. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d5875418fd7eb25808f8708537f8a17008502cf24bf3ffcd808595f95f5d8d4
4
- data.tar.gz: c695e3f997ddb7284324f8c990a910083b183bb6d379046c27258bd4ceeb09d4
3
+ metadata.gz: 199c0adec648e1556593cb36414f58482fdf2f638f59fad25ca04ee43c0de02c
4
+ data.tar.gz: f8a7d728594094a46cfcac9ba2306ebd351bc3d80b785eabd1743252a7f6588e
5
5
  SHA512:
6
- metadata.gz: c69b0dad9c16c852b58d98faf6356a1f19704126f9963605afd002b7b186b7fdec26511219a68a7d111b4e4e718ca2a5a1415f01121046b0fe67a81bc20d8b6e
7
- data.tar.gz: 2bc760ccdcf01d2ca3bb02489e4d12dc390af9e077bc35904d0147b24c4761f99632715c9b6c9ae2789cd48041191e644498681e059251ebe4476719e98a83c7
6
+ metadata.gz: b148e272ab33acca1a14b5523c448411d92882a6069ba183522e223077ed5185854feeca378608d68009abaf08ee9a1bcffada932cb42e006455992d07e98eb7
7
+ data.tar.gz: a42507f48287500895380ded3951be855dcfd24311933efc9a50e79c3c81a230ab32a9f9378d7de9aa33a9778f4e15c5f5d55e94c0f8f8f60bae519cc51f6ece
data/README.md CHANGED
@@ -70,7 +70,7 @@ Usage counts against the anniversary of your subscription, not the calendar mont
70
70
 
71
71
  ### Batch lookup
72
72
 
73
- Look up many addresses at once. Bogons and cached answers are handled locally, and everything else goes to the batch endpoint in chunks of up to 1000 addresses, in parallel:
73
+ Look up as many addresses as you like at once. Bogons and cached answers are handled locally, and everything else goes to the batch endpoint in chunks of up to 1000 addresses, in parallel:
74
74
 
75
75
  ```ruby
76
76
  results = client.lookup_batch(['45.83.91.1', '8.8.8.8', '1.1.1.1'])
@@ -86,10 +86,10 @@ end
86
86
 
87
87
  Results are keyed by address, in the order you first listed each one, so duplicates in your list collapse into a single entry and one address failing never loses the rest: it carries its error as its value, with the status the API would have given that address on its own.
88
88
 
89
- How many chunks are in flight at once, and how many times a failed chunk is retried, are configurable per call:
89
+ How many chunks are in flight at once, how many times a failed chunk is retried, and how long each chunk's request may take are configurable per call:
90
90
 
91
91
  ```ruby
92
- results = client.lookup_batch(many_ips, concurrency: 4, retries: 4)
92
+ results = client.lookup_batch(many_ips, concurrency: 4, retries: 4, timeout: 5)
93
93
  ```
94
94
 
95
95
  ### Caching
@@ -152,6 +152,16 @@ end
152
152
 
153
153
  Note that `:rate_limited` and `:quota_exceeded` both arrive as HTTP 429 and are not the same thing. A rate limit is when the API faces extreme traffic bursts and so retrying later works; but a spent quota needs your allowance raised or the window to roll over. The library retries rate limits for you, but not if your quota is exceeded.
154
154
 
155
+ ### Timeouts and retries
156
+
157
+ ```ruby
158
+ client = VPNDetection::Client.new(timeout: 10, retries: 4)
159
+
160
+ result = client.lookup('45.83.91.1', timeout: 2, retries: 0)
161
+ ```
162
+
163
+ `timeout` is in seconds and bounds each attempt, body included, so a call that is retried can take longer in total. It defaults to 30 seconds; before 5.2.0 the default was 10, so pass `timeout: 10` to keep that bound. The client's values are defaults: `lookup`, `lookup_batch`, `my_ip` and `my_entitlement` each take `timeout:` and `retries:` for that call alone, and every `client.oauth` method takes `timeout:`. A database download bounds only its connection with it, because a whole transfer can take minutes.
164
+
155
165
  ### Database downloads
156
166
 
157
167
  If your key carries the `db.download` scope, the licensed databases are available through `client.database`. A license covers a database FAMILY and a download names one of its versions, so the id comes from `versions`:
@@ -167,6 +177,23 @@ bytes = client.database.download_bytes('cdn_ip_v1', 'csvgz')
167
177
 
168
178
  `download` streams straight to disk, so nothing bigger than a chunk is ever held in memory whatever the database weighs, and it writes through a neighboring `.part` file so a transfer that dies half way leaves no truncated copy behind. `download_url` hands back the time-limited link and follows nothing, for when you want to run the transfer yourself. `download_bytes` holds the whole file in memory, and the catalog runs from `cdn_ip_v1` at 10 KB to `resproxy_ip_90d_v1` at 1.79 GB, so reach for `download` for anything you have not measured.
169
179
 
180
+ ### Sign in with OAuth (device flow)
181
+
182
+ A program running on a person's own machine can let them sign in with their browser and pick one of their API keys, instead of asking them to paste one.
183
+
184
+ ```ruby
185
+ client = VPNDetection::Client.new
186
+ device = client.oauth.device_authorization('your-client-id', scope: 'account.read apikeys.read apikeys.reveal')
187
+ puts "Open #{device.verification_uri} and enter #{device.user_code}"
188
+
189
+ token = client.oauth.poll_device_token('your-client-id', device)
190
+ raise 'no API key was picked' if token.apikey.nil?
191
+
192
+ keyed = VPNDetection::Client.new(api_key: token.apikey)
193
+ ```
194
+
195
+ `poll_device_token` raises `VPNDetection::OauthAccessDeniedError` when the person refuses and `VPNDetection::OauthExpiredTokenError` when the code expires first. Client IDs are issued on request from support@vpndetection.io, and `client.oauth.revoke('your-client-id', token.refresh_token)` signs the machine out.
196
+
170
197
  ### Absent is not false
171
198
 
172
199
  Every field beyond `ip` and `is_vpn` is present when your plan includes it and `nil` when it does not. `nil` means "not in your plan"; `false` means "we checked, and no".
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -13,10 +13,12 @@ Generator version: 7.25.0
13
13
  require 'cgi'
14
14
 
15
15
  module VPNDetection
16
+ # @deprecated Since 5.2.0, and removed in the next major. Use {VPNDetection::Client#oauth}.
16
17
  class AuthorizationWireApi
17
18
  attr_accessor :api_client
18
19
 
19
20
  def initialize(api_client = ApiClient.default)
21
+ warn("#{self.class} is deprecated; use VPNDetection::Client#oauth", category: :deprecated)
20
22
  @api_client = api_client
21
23
  end
22
24
  # Authorize
@@ -257,9 +259,9 @@ module VPNDetection
257
259
 
258
260
  # Revoke
259
261
  # RFC 7009. Always answers 200, including for a token that was never valid - an endpoint that distinguished the two would be a way to test whether a stolen string is a live credential. Revoking a REFRESH token ends the whole authorization and takes its access tokens with it. Revoking an access token affects only that token.
260
- # @param token [String]
262
+ # @param token [String] An access token or a refresh token.
261
263
  # @param [Hash] opts the optional parameters
262
- # @option opts [String] :client_id
264
+ # @option opts [String] :client_id Accepted and not checked.
263
265
  # @return [Object]
264
266
  def oauth_revoke(token, opts = {})
265
267
  data, _status_code, _headers = oauth_revoke_with_http_info(token, opts)
@@ -268,9 +270,9 @@ module VPNDetection
268
270
 
269
271
  # Revoke
270
272
  # RFC 7009. Always answers 200, including for a token that was never valid - an endpoint that distinguished the two would be a way to test whether a stolen string is a live credential. Revoking a REFRESH token ends the whole authorization and takes its access tokens with it. Revoking an access token affects only that token.
271
- # @param token [String]
273
+ # @param token [String] An access token or a refresh token.
272
274
  # @param [Hash] opts the optional parameters
273
- # @option opts [String] :client_id
275
+ # @option opts [String] :client_id Accepted and not checked.
274
276
  # @return [Array<(Object, Integer, Hash)>] Object data, response status code and response headers
275
277
  def oauth_revoke_with_http_info(token, opts = {})
276
278
  if @api_client.config.debugging
@@ -329,14 +331,14 @@ module VPNDetection
329
331
 
330
332
  # Token
331
333
  # Three grant types. `urn:ietf:params:oauth:grant-type:device_code` polls a device authorization. Until the person approves it answers `authorization_pending`; poll faster than `interval` and it answers `slow_down`, which means widen your interval and keep it widened. `authorization_code` exchanges a code from `/oauth/authorize`, with the `code_verifier` matching the challenge you sent. `refresh_token` exchanges a refresh token. The presented token is consumed whatever happens next, so store the new one before using it.
332
- # @param grant_type [String]
334
+ # @param grant_type [String] &#x60;urn:ietf:params:oauth:grant-type:device_code&#x60;, &#x60;authorization_code&#x60; or &#x60;refresh_token&#x60;.
333
335
  # @param client_id [String]
334
336
  # @param [Hash] opts the optional parameters
335
- # @option opts [String] :device_code
336
- # @option opts [String] :code
337
- # @option opts [String] :code_verifier
338
- # @option opts [String] :redirect_uri
339
- # @option opts [String] :refresh_token
337
+ # @option opts [String] :device_code Required by the device code grant.
338
+ # @option opts [String] :code Required by the authorization code grant.
339
+ # @option opts [String] :code_verifier Required by the authorization code grant.
340
+ # @option opts [String] :redirect_uri Authorization code grant: the &#x60;redirect_uri&#x60; the code was issued against, exactly.
341
+ # @option opts [String] :refresh_token Required by the refresh token grant.
340
342
  # @return [TokenResponse]
341
343
  def oauth_token(grant_type, client_id, opts = {})
342
344
  data, _status_code, _headers = oauth_token_with_http_info(grant_type, client_id, opts)
@@ -345,14 +347,14 @@ module VPNDetection
345
347
 
346
348
  # Token
347
349
  # Three grant types. &#x60;urn:ietf:params:oauth:grant-type:device_code&#x60; polls a device authorization. Until the person approves it answers &#x60;authorization_pending&#x60;; poll faster than &#x60;interval&#x60; and it answers &#x60;slow_down&#x60;, which means widen your interval and keep it widened. &#x60;authorization_code&#x60; exchanges a code from &#x60;/oauth/authorize&#x60;, with the &#x60;code_verifier&#x60; matching the challenge you sent. &#x60;refresh_token&#x60; exchanges a refresh token. The presented token is consumed whatever happens next, so store the new one before using it.
348
- # @param grant_type [String]
350
+ # @param grant_type [String] &#x60;urn:ietf:params:oauth:grant-type:device_code&#x60;, &#x60;authorization_code&#x60; or &#x60;refresh_token&#x60;.
349
351
  # @param client_id [String]
350
352
  # @param [Hash] opts the optional parameters
351
- # @option opts [String] :device_code
352
- # @option opts [String] :code
353
- # @option opts [String] :code_verifier
354
- # @option opts [String] :redirect_uri
355
- # @option opts [String] :refresh_token
353
+ # @option opts [String] :device_code Required by the device code grant.
354
+ # @option opts [String] :code Required by the authorization code grant.
355
+ # @option opts [String] :code_verifier Required by the authorization code grant.
356
+ # @option opts [String] :redirect_uri Authorization code grant: the &#x60;redirect_uri&#x60; the code was issued against, exactly.
357
+ # @option opts [String] :refresh_token Required by the refresh token grant.
356
358
  # @return [Array<(TokenResponse, Integer, Hash)>] TokenResponse data, response status code and response headers
357
359
  def oauth_token_with_http_info(grant_type, client_id, opts = {})
358
360
  if @api_client.config.debugging
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -285,7 +285,7 @@ module VPNDetection
285
285
  # History
286
286
  # Recent download attempts for this organization, newest first. Refusals are listed too, so a failed transfer can be accounted for.
287
287
  # @param [Hash] opts the optional parameters
288
- # @option opts [Integer] :limit (default to 50)
288
+ # @option opts [Integer] :limit How many attempts to return. Clamped to 1 through 200. (default to 50)
289
289
  # @return [DownloadList]
290
290
  def list_downloads(opts = {})
291
291
  data, _status_code, _headers = list_downloads_with_http_info(opts)
@@ -295,7 +295,7 @@ module VPNDetection
295
295
  # History
296
296
  # Recent download attempts for this organization, newest first. Refusals are listed too, so a failed transfer can be accounted for.
297
297
  # @param [Hash] opts the optional parameters
298
- # @option opts [Integer] :limit (default to 50)
298
+ # @option opts [Integer] :limit How many attempts to return. Clamped to 1 through 200. (default to 50)
299
299
  # @return [Array<(DownloadList, Integer, Hash)>] DownloadList data, response status code and response headers
300
300
  def list_downloads_with_http_info(opts = {})
301
301
  if @api_client.config.debugging
@@ -305,6 +305,10 @@ module VPNDetection
305
305
  fail ArgumentError, 'invalid value for "opts[:"limit"]" when calling DatabaseWireApi.list_downloads, must be smaller than or equal to 200.'
306
306
  end
307
307
 
308
+ if @api_client.config.client_side_validation && !opts[:'limit'].nil? && opts[:'limit'] < 1
309
+ fail ArgumentError, 'invalid value for "opts[:"limit"]" when calling DatabaseWireApi.list_downloads, must be greater than or equal to 1.'
310
+ end
311
+
308
312
  # resource path
309
313
  local_var_path = '/api/v1/database/downloads'
310
314
 
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -8,7 +8,7 @@ module VPNDetection
8
8
  DEFAULT_CACHE_TTL = 3600
9
9
  DEFAULT_CONCURRENCY = 8
10
10
  DEFAULT_RETRIES = 2
11
- DEFAULT_TIMEOUT = 10
11
+ DEFAULT_TIMEOUT = 30
12
12
  # The most addresses POST /batch takes in one call; a larger batch is sent in
13
13
  # chunks of this size.
14
14
  BATCH_MAX = 1000
@@ -20,6 +20,8 @@ module VPNDetection
20
20
  class Client
21
21
  # The licensed dataset downloads, for keys that carry the `db.download` scope.
22
22
  attr_reader :database
23
+ # Signing a person in with OAuth, which needs no API key at all.
24
+ attr_reader :oauth
23
25
 
24
26
  # @param api_key [String, nil] omit it entirely to use the free tier, which
25
27
  # answers `ip` and `is_vpn` and allows 1000 requests per day per source
@@ -28,6 +30,10 @@ module VPNDetection
28
30
  # @param cache_ttl [Numeric] how long an answer stays fresh, in seconds.
29
31
  # @param concurrency [Integer] batch requests - chunks of up to 1000 addresses - in flight during a batch.
30
32
  # @param retries [Integer] extra attempts for a transient failure.
33
+ # @param timeout [Numeric] seconds one request may take before it is
34
+ # abandoned. Applies per ATTEMPT, so a retried call may take longer in
35
+ # total, and every call that takes `retries:` also takes `timeout:` to
36
+ # override it. A dataset transfer bounds only its connect phase with it.
31
37
  # @param transport [Transport, nil] override the HTTP layer, mostly for tests.
32
38
  def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, cache: true,
33
39
  cache_max_size: DEFAULT_CACHE_MAX_SIZE, cache_ttl: DEFAULT_CACHE_TTL,
@@ -40,6 +46,7 @@ module VPNDetection
40
46
  @concurrency = concurrency
41
47
  @retries = retries
42
48
  @database = DatabaseApi.new(@transport, retries: retries)
49
+ @oauth = OauthApi.new(@transport, retries: retries)
43
50
  end
44
51
 
45
52
  # Whether an address is private, loopback, link-local, documentation,
@@ -57,14 +64,17 @@ module VPNDetection
57
64
  #
58
65
  # A bogon is answered locally and never reaches the network. Everything else
59
66
  # is served, then cached for this instance.
60
- def lookup(ip, retries: nil)
67
+ #
68
+ # @param retries [Integer, nil] extra attempts, for THIS call only.
69
+ # @param timeout [Numeric, nil] seconds each attempt may take, for THIS call only.
70
+ def lookup(ip, retries: nil, timeout: nil)
61
71
  return Bogon.result(ip) if Bogon.bogon?(ip)
62
72
 
63
73
  hit = @cache&.get(ip)
64
74
  return hit unless hit.nil?
65
75
 
66
76
  result = Retries.with_retries(retries || @retries) do
67
- Transport.lookup_result(@transport.lookup_request(ip).run)
77
+ Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
68
78
  end
69
79
  @cache&.set(ip, result)
70
80
  result
@@ -80,9 +90,9 @@ module VPNDetection
80
90
  # Deliberately NOT cached. The cache is keyed by address, and which address
81
91
  # this is IS the question: a machine that moves between networks would
82
92
  # otherwise be told where it used to be.
83
- def my_ip(retries: nil)
93
+ def my_ip(retries: nil, timeout: nil)
84
94
  Retries.with_retries(retries || @retries) do
85
- Transport.lookup_result(@transport.myip_request.run)
95
+ Transport.lookup_result(@transport.myip_request(timeout: timeout).run)
86
96
  end
87
97
  end
88
98
 
@@ -105,9 +115,9 @@ module VPNDetection
105
115
  # cached answer is a wrong one within seconds of the next request.
106
116
  #
107
117
  # @return [Entitlement]
108
- def my_entitlement(retries: nil)
118
+ def my_entitlement(retries: nil, timeout: nil)
109
119
  Retries.with_retries(retries || @retries) do
110
- Transport.entitlement_result(@transport.entitlement_request.run)
120
+ Transport.entitlement_result(@transport.entitlement_request(timeout: timeout).run)
111
121
  end
112
122
  end
113
123
 
@@ -122,10 +132,17 @@ module VPNDetection
122
132
  # per-entry failure with the status the single lookup would have answered,
123
133
  # and a chunk that fails as a whole marks every address in it.
124
134
  #
125
- # @param concurrency [Integer, nil] chunks in flight, for THIS batch only.
135
+ # @param concurrency [Integer, nil] chunks in flight, for THIS batch only. Below 1
136
+ # is refused as `:bad_request` before any request, since nothing could ever run.
126
137
  # @param retries [Integer, nil] extra attempts for a failed chunk, for THIS batch only.
138
+ # @param timeout [Numeric, nil] seconds each chunk's attempt may take, for THIS batch only.
127
139
  # @return [Hash{String => Result, Error}] in the order the addresses were given
128
- def lookup_batch(ips, concurrency: nil, retries: nil)
140
+ def lookup_batch(ips, concurrency: nil, retries: nil, timeout: nil)
141
+ limit = concurrency || @concurrency
142
+ unless limit.is_a?(Numeric) && limit >= 1
143
+ raise Error.new(:bad_request, "concurrency must be at least 1, not #{limit.inspect}")
144
+ end
145
+
129
146
  addresses = ips.to_a.uniq
130
147
  answers = {}
131
148
  pending = []
@@ -135,8 +152,7 @@ module VPNDetection
135
152
  hit.nil? ? pending << ip : answers[ip] = hit
136
153
  end
137
154
  unless pending.empty?
138
- run_batch(pending.each_slice(BATCH_MAX).to_a, answers,
139
- concurrency || @concurrency, retries || @retries)
155
+ run_batch(pending.each_slice(BATCH_MAX).to_a, answers, limit, retries || @retries, timeout)
140
156
  end
141
157
 
142
158
  # Reinstated in input order: a hydra settles in completion order, and a
@@ -150,12 +166,12 @@ module VPNDetection
150
166
  # would silently cap a per-call concurrency at the client's setting, and
151
167
  # would not be safe to drive from two threads either. Each request is one
152
168
  # chunk of up to 1000 addresses.
153
- def run_batch(chunks, answers, concurrency, retries)
169
+ def run_batch(chunks, answers, concurrency, retries, timeout)
154
170
  hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
155
171
  attempts = Hash.new(0)
156
172
 
157
173
  enqueue = lambda do |chunk|
158
- request = @transport.batch_request(chunk)
174
+ request = @transport.batch_request(chunk, timeout: timeout)
159
175
  request.on_complete do |response|
160
176
  outcome = settle(chunk, response, attempts, retries, enqueue)
161
177
  answers.merge!(outcome) unless outcome.nil?
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -63,11 +63,7 @@ module VPNDetection
63
63
  partial = "#{path}.part"
64
64
  begin
65
65
  url = download_url(id, format)
66
- written = Retries.with_retries(@retries) do
67
- # Reopened per attempt, so a retry restarts the file rather than
68
- # appending a second copy of the body to a half-written one.
69
- File.open(partial, 'wb') { |file| stream(url) { |chunk| file.write(chunk) } }
70
- end
66
+ written = File.open(partial, 'wb') { |file| transfer(url) { |chunk| file.write(chunk) } }
71
67
  File.rename(partial, path)
72
68
  rescue StandardError
73
69
  File.delete(partial) if File.exist?(partial)
@@ -84,11 +80,9 @@ module VPNDetection
84
80
  # and use {#download} for anything you have not measured.
85
81
  def download_bytes(id, format)
86
82
  url = download_url(id, format)
87
- Retries.with_retries(@retries) do
88
- bytes = String.new(encoding: Encoding::BINARY)
89
- stream(url) { |chunk| bytes << chunk }
90
- bytes
91
- end
83
+ bytes = String.new(encoding: Encoding::BINARY)
84
+ transfer(url) { |chunk| bytes << chunk }
85
+ bytes
92
86
  end
93
87
 
94
88
  private
@@ -106,6 +100,20 @@ module VPNDetection
106
100
  "invalid value for \"format\", must be one of #{DatabaseFormat.all_vars}"
107
101
  end
108
102
 
103
+ # The transfer of a presigned link, retried only while nothing has reached the
104
+ # block: object storage failing before the body is as transient as any
105
+ # outage, while a body that dies part way is not fetched again, because the
106
+ # bytes already handed over cannot be taken back.
107
+ def transfer(url, &sink)
108
+ delivered = false
109
+ Retries.with_retries(@retries, retry_if: -> { !delivered }) do
110
+ stream(url) do |chunk|
111
+ delivered = true
112
+ sink.call(chunk)
113
+ end
114
+ end
115
+ end
116
+
109
117
  # Runs one transfer of a presigned link, handing each chunk to the block, and
110
118
  # returns the bytes that reached it.
111
119
  #
@@ -38,25 +38,26 @@ module VPNDetection
38
38
  def self.from_status(status, headers, body, message: nil)
39
39
  message ||= message_of(body) || "request failed with status #{status}"
40
40
  retry_after = parse_retry_after(header(headers, 'retry-after'))
41
+ kind = kind_for(status, headers)
41
42
 
43
+ new(kind, message, status: status, retry_after_seconds: kind == :rate_limited ? retry_after : nil)
44
+ end
45
+
46
+ # What a response with this status is, whatever its body says.
47
+ def self.kind_for(status, headers)
42
48
  case status
43
- when 429
44
- # Present means transient, absent means an allowance is spent. Nothing
45
- # else in the response separates the two.
46
- if retry_after.nil?
47
- new(:quota_exceeded, message, status: status)
48
- else
49
- new(:rate_limited, message, status: status, retry_after_seconds: retry_after)
50
- end
51
- when 400 then new(:bad_request, message, status: status)
52
- when 401 then new(:unauthorized, message, status: status)
53
- when 403 then new(:forbidden, message, status: status)
49
+ # Present means transient, absent means an allowance is spent. Nothing
50
+ # else in the response separates the two.
51
+ when 429 then parse_retry_after(header(headers, 'retry-after')).nil? ? :quota_exceeded : :rate_limited
52
+ when 400 then :bad_request
53
+ when 401 then :unauthorized
54
+ when 403 then :forbidden
54
55
  # Every other 4xx is a CLIENT error. Classifying on the RANGE rather than
55
56
  # on an enumerated list is what keeps a 404 from a bad dataset id falling
56
57
  # through to the retryable server_error default and being retried twice
57
58
  # before it fails.
58
- when 400..499 then new(:bad_request, message, status: status)
59
- else new(:server_error, message, status: status)
59
+ when 400..499 then :bad_request
60
+ else :server_error
60
61
  end
61
62
  end
62
63
 
@@ -111,4 +112,44 @@ module VPNDetection
111
112
 
112
113
  private_class_method :header, :message_of, :parse_retry_after
113
114
  end
115
+
116
+ # The authorization server refusing an OAuth request: a 4xx whose body names an
117
+ # RFC 6749 `error` code. Never retryable, whatever the status, because every
118
+ # code it can carry answers the request as it was made.
119
+ #
120
+ # Not `OauthError`: that constant is the generated model of the error body.
121
+ class OauthRequestError < Error
122
+ # The `error` code, such as `slow_down` or `invalid_grant`.
123
+ attr_reader :error_code
124
+ # `error_description` when the server sent one as a string, otherwise nil.
125
+ attr_reader :error_description
126
+
127
+ # The refusal a response carries, as the most specific class its code has.
128
+ def self.for_code(error_code, error_description, status:, headers: {})
129
+ klass = case error_code
130
+ when 'access_denied' then OauthAccessDeniedError
131
+ when 'expired_token' then OauthExpiredTokenError
132
+ else OauthRequestError
133
+ end
134
+ klass.new(error_code, error_description, kind: Error.kind_for(status, headers), status: status)
135
+ end
136
+
137
+ def initialize(error_code, error_description = nil, kind: :bad_request, status: nil)
138
+ super(kind, error_description.nil? ? error_code : "#{error_code}: #{error_description}", status: status)
139
+ @error_code = error_code
140
+ @error_description = error_description
141
+ end
142
+
143
+ def retryable?
144
+ false
145
+ end
146
+ end
147
+
148
+ # The person refused the sign-in. Their device code is spent.
149
+ class OauthAccessDeniedError < OauthRequestError; end
150
+
151
+ # The device code is no longer valid: it expired, or was already exchanged or
152
+ # refused. Raised with no status when {OauthApi#poll_device_token} reaches the
153
+ # code's lifetime before the server says so.
154
+ class OauthExpiredTokenError < OauthRequestError; end
114
155
  end
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0
@@ -3,7 +3,7 @@
3
3
 
4
4
  #The VPNDetection API: classify any IP address, and download the databases behind the answers. See https://docs.vpndetection.io for guides and https://github.com/vpndetection-io for the official client libraries.
5
5
 
6
- The version of the OpenAPI document: 2026.09.15
6
+ The version of the OpenAPI document: 2026.09.16
7
7
  Contact: support@vpndetection.io
8
8
  Generated by: https://openapi-generator.tech
9
9
  Generator version: 7.25.0