vpndetection 5.0.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 +31 -4
  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 +69 -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 +77 -30
  14. data/lib/vpndetection/configuration.rb +1 -1
  15. data/lib/vpndetection/database_api.rb +18 -10
  16. data/lib/vpndetection/errors.rb +61 -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 +193 -0
  28. data/lib/vpndetection/models/batch_lookup_request.rb +185 -0
  29. data/lib/vpndetection/models/batch_lookup_response.rb +196 -0
  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 +80 -4
  61. data/lib/vpndetection/version.rb +1 -1
  62. data/lib/vpndetection.rb +1 -0
  63. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aa0cdfbbc74c41bcfe5ecb123df7dc02f4d9255229d5b96764cd3046f290004a
4
- data.tar.gz: 540b91db33957765bed48ab55b3cacc10dab5b55fa8d8a91d51ad21a3cee86de
3
+ metadata.gz: 199c0adec648e1556593cb36414f58482fdf2f638f59fad25ca04ee43c0de02c
4
+ data.tar.gz: f8a7d728594094a46cfcac9ba2306ebd351bc3d80b785eabd1743252a7f6588e
5
5
  SHA512:
6
- metadata.gz: 020525503bb0410d9c9a79b04230b6ce2956798813583d7dee1dd4f92c3f00cee496a273a5d8a4db240a1661d19674d939e8c0ceb9a2949669c2793eb3170a97
7
- data.tar.gz: c0071cc728e2a67f3da1e6c33974ef223bc46e8b96057ea4a013909f408fc781246ba9ec4216ec35bd34001428d004f0d7709ea9957dac2689398b8c27349e15
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
- You can do batch lookups with a list, which parallelizes requests for you efficiently:
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'])
@@ -84,12 +84,12 @@ results.each do |ip, result|
84
84
  end
85
85
  ```
86
86
 
87
- Results are keyed by address, so duplicates in your list collapse into a single request and one address failing never loses the rest.
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
- Concurrency and other variables 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: 32, 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
@@ -19,6 +19,74 @@ module VPNDetection
19
19
  def initialize(api_client = ApiClient.default)
20
20
  @api_client = api_client
21
21
  end
22
+ # Batch
23
+ # Answers up to 1000 addresses in one call. Each distinct string in `ips` is one lookup: it costs exactly what `GET /{ip}` costs for that address and comes back with exactly the fields that call would carry for your plan. Exact duplicates collapse to one entry and one lookup. Both maps in the answer are keyed by the string you sent, so nothing has to be lined up by position; the `ip` inside each result is the normalized form. An address that could not be answered sits in `errors` with the status and message the single lookup would have given, and never disturbs the others: a string that is not an address is a `400` there, and an allowance that runs out part way through leaves the remaining entries as `429`s. The call itself fails only for the reasons below, and a `429` on the call always carries `Retry-After`: the batch is admitted or refused whole by the rate limit, so a per-entry `429` is always a spent allowance and never a throttle.
24
+ # @param batch_lookup_request [BatchLookupRequest]
25
+ # @param [Hash] opts the optional parameters
26
+ # @return [BatchLookupResponse]
27
+ def lookup_batch(batch_lookup_request, opts = {})
28
+ data, _status_code, _headers = lookup_batch_with_http_info(batch_lookup_request, opts)
29
+ data
30
+ end
31
+
32
+ # Batch
33
+ # Answers up to 1000 addresses in one call. Each distinct string in &#x60;ips&#x60; is one lookup: it costs exactly what &#x60;GET /{ip}&#x60; costs for that address and comes back with exactly the fields that call would carry for your plan. Exact duplicates collapse to one entry and one lookup. Both maps in the answer are keyed by the string you sent, so nothing has to be lined up by position; the &#x60;ip&#x60; inside each result is the normalized form. An address that could not be answered sits in &#x60;errors&#x60; with the status and message the single lookup would have given, and never disturbs the others: a string that is not an address is a &#x60;400&#x60; there, and an allowance that runs out part way through leaves the remaining entries as &#x60;429&#x60;s. The call itself fails only for the reasons below, and a &#x60;429&#x60; on the call always carries &#x60;Retry-After&#x60;: the batch is admitted or refused whole by the rate limit, so a per-entry &#x60;429&#x60; is always a spent allowance and never a throttle.
34
+ # @param batch_lookup_request [BatchLookupRequest]
35
+ # @param [Hash] opts the optional parameters
36
+ # @return [Array<(BatchLookupResponse, Integer, Hash)>] BatchLookupResponse data, response status code and response headers
37
+ def lookup_batch_with_http_info(batch_lookup_request, opts = {})
38
+ if @api_client.config.debugging
39
+ @api_client.config.logger.debug 'Calling API: LookupWireApi.lookup_batch ...'
40
+ end
41
+ # verify the required parameter 'batch_lookup_request' is set
42
+ if @api_client.config.client_side_validation && batch_lookup_request.nil?
43
+ fail ArgumentError, "Missing the required parameter 'batch_lookup_request' when calling LookupWireApi.lookup_batch"
44
+ end
45
+ # resource path
46
+ local_var_path = '/batch'
47
+
48
+ # query parameters
49
+ query_params = opts[:query_params] || {}
50
+
51
+ # header parameters
52
+ header_params = opts[:header_params] || {}
53
+ # HTTP header 'Accept' (if needed)
54
+ header_params['Accept'] = @api_client.select_header_accept(['application/json']) unless header_params['Accept']
55
+ # HTTP header 'Content-Type'
56
+ content_type = @api_client.select_header_content_type(['application/json'])
57
+ if !content_type.nil?
58
+ header_params['Content-Type'] = content_type
59
+ end
60
+
61
+ # form parameters
62
+ form_params = opts[:form_params] || {}
63
+
64
+ # http body (model)
65
+ post_body = opts[:debug_body] || @api_client.object_to_http_body(batch_lookup_request)
66
+
67
+ # return_type
68
+ return_type = opts[:debug_return_type] || 'BatchLookupResponse'
69
+
70
+ # auth_names
71
+ auth_names = opts[:debug_auth_names] || ['apiKeyQuery', 'bearerAuth', 'apiKeyHeader']
72
+
73
+ new_options = opts.merge(
74
+ :operation => :"LookupWireApi.lookup_batch",
75
+ :header_params => header_params,
76
+ :query_params => query_params,
77
+ :form_params => form_params,
78
+ :body => post_body,
79
+ :auth_names => auth_names,
80
+ :return_type => return_type
81
+ )
82
+
83
+ data, status_code, headers = @api_client.call_api(:POST, local_var_path, new_options)
84
+ if @api_client.config.debugging
85
+ @api_client.config.logger.debug "API called: LookupWireApi#lookup_batch\nData: #{data.inspect}\nStatus code: #{status_code}\nHeaders: #{headers}"
86
+ end
87
+ return data, status_code, headers
88
+ end
89
+
22
90
  # Lookup
23
91
  # Answers what is known about a single IPv4 or IPv6 address. Which fields come back is decided by the plan behind the presented key; with no key the answer is `ip` and `is_vpn`.
24
92
  # @param ip [String] The IPv4 or IPv6 address to classify.
@@ -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,10 @@ 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
+ # The most addresses POST /batch takes in one call; a larger batch is sent in
13
+ # chunks of this size.
14
+ BATCH_MAX = 1000
12
15
 
13
16
  # A client for the VPNDetection API.
14
17
  #
@@ -17,14 +20,20 @@ module VPNDetection
17
20
  class Client
18
21
  # The licensed dataset downloads, for keys that carry the `db.download` scope.
19
22
  attr_reader :database
23
+ # Signing a person in with OAuth, which needs no API key at all.
24
+ attr_reader :oauth
20
25
 
21
26
  # @param api_key [String, nil] omit it entirely to use the free tier, which
22
27
  # answers `ip` and `is_vpn` and allows 1000 requests per day per source
23
28
  # address.
24
29
  # @param cache [Boolean] pass false to disable caching.
25
30
  # @param cache_ttl [Numeric] how long an answer stays fresh, in seconds.
26
- # @param concurrency [Integer] in-flight requests during a batch.
31
+ # @param concurrency [Integer] batch requests - chunks of up to 1000 addresses - in flight during a batch.
27
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.
28
37
  # @param transport [Transport, nil] override the HTTP layer, mostly for tests.
29
38
  def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, cache: true,
30
39
  cache_max_size: DEFAULT_CACHE_MAX_SIZE, cache_ttl: DEFAULT_CACHE_TTL,
@@ -37,6 +46,7 @@ module VPNDetection
37
46
  @concurrency = concurrency
38
47
  @retries = retries
39
48
  @database = DatabaseApi.new(@transport, retries: retries)
49
+ @oauth = OauthApi.new(@transport, retries: retries)
40
50
  end
41
51
 
42
52
  # Whether an address is private, loopback, link-local, documentation,
@@ -54,14 +64,17 @@ module VPNDetection
54
64
  #
55
65
  # A bogon is answered locally and never reaches the network. Everything else
56
66
  # is served, then cached for this instance.
57
- 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)
58
71
  return Bogon.result(ip) if Bogon.bogon?(ip)
59
72
 
60
73
  hit = @cache&.get(ip)
61
74
  return hit unless hit.nil?
62
75
 
63
76
  result = Retries.with_retries(retries || @retries) do
64
- Transport.lookup_result(@transport.lookup_request(ip).run)
77
+ Transport.lookup_result(@transport.lookup_request(ip, timeout: timeout).run)
65
78
  end
66
79
  @cache&.set(ip, result)
67
80
  result
@@ -77,9 +90,9 @@ module VPNDetection
77
90
  # Deliberately NOT cached. The cache is keyed by address, and which address
78
91
  # this is IS the question: a machine that moves between networks would
79
92
  # otherwise be told where it used to be.
80
- def my_ip(retries: nil)
93
+ def my_ip(retries: nil, timeout: nil)
81
94
  Retries.with_retries(retries || @retries) do
82
- Transport.lookup_result(@transport.myip_request.run)
95
+ Transport.lookup_result(@transport.myip_request(timeout: timeout).run)
83
96
  end
84
97
  end
85
98
 
@@ -102,21 +115,34 @@ module VPNDetection
102
115
  # cached answer is a wrong one within seconds of the next request.
103
116
  #
104
117
  # @return [Entitlement]
105
- def my_entitlement(retries: nil)
118
+ def my_entitlement(retries: nil, timeout: nil)
106
119
  Retries.with_retries(retries || @retries) do
107
- Transport.entitlement_result(@transport.entitlement_request.run)
120
+ Transport.entitlement_result(@transport.entitlement_request(timeout: timeout).run)
108
121
  end
109
122
  end
110
123
 
111
- # Classify many addresses in parallel.
124
+ # Classify many addresses in as few requests as possible.
112
125
  #
113
- # Keyed by address rather than positional, so duplicates in the input
114
- # collapse to a single request and the caller never has to line two lists
115
- # up. An address that fails carries its error as its value, so one bad entry
116
- # cannot lose the rest of the answers.
126
+ # Bogons are answered locally and cached answers are reused; everything else
127
+ # goes to the batch endpoint in chunks of up to 1000 addresses, with at most
128
+ # `concurrency` chunks in flight. Keyed by address rather than positional, so
129
+ # duplicates in the input collapse to a single entry and the caller never has
130
+ # to line two lists up. An address that fails carries its error as its value,
131
+ # so one bad entry cannot lose the rest of the answers: the API reports a
132
+ # per-entry failure with the status the single lookup would have answered,
133
+ # and a chunk that fails as a whole marks every address in it.
117
134
  #
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.
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.
118
139
  # @return [Hash{String => Result, Error}] in the order the addresses were given
119
- 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
+
120
146
  addresses = ips.to_a.uniq
121
147
  answers = {}
122
148
  pending = []
@@ -126,7 +152,7 @@ module VPNDetection
126
152
  hit.nil? ? pending << ip : answers[ip] = hit
127
153
  end
128
154
  unless pending.empty?
129
- run_batch(pending, answers, concurrency || @concurrency, retries || @retries)
155
+ run_batch(pending.each_slice(BATCH_MAX).to_a, answers, limit, retries || @retries, timeout)
130
156
  end
131
157
 
132
158
  # Reinstated in input order: a hydra settles in completion order, and a
@@ -138,37 +164,58 @@ module VPNDetection
138
164
 
139
165
  # One hydra per call, sized for THIS call. Reusing an instance-level hydra
140
166
  # would silently cap a per-call concurrency at the client's setting, and
141
- # would not be safe to drive from two threads either.
142
- def run_batch(pending, answers, concurrency, retries)
167
+ # would not be safe to drive from two threads either. Each request is one
168
+ # chunk of up to 1000 addresses.
169
+ def run_batch(chunks, answers, concurrency, retries, timeout)
143
170
  hydra = Typhoeus::Hydra.new(max_concurrency: concurrency)
144
171
  attempts = Hash.new(0)
145
172
 
146
- enqueue = lambda do |ip|
147
- request = @transport.lookup_request(ip)
173
+ enqueue = lambda do |chunk|
174
+ request = @transport.batch_request(chunk, timeout: timeout)
148
175
  request.on_complete do |response|
149
- outcome = settle(ip, response, attempts, retries, enqueue)
150
- answers[ip] = outcome unless outcome.nil?
176
+ outcome = settle(chunk, response, attempts, retries, enqueue)
177
+ answers.merge!(outcome) unless outcome.nil?
151
178
  end
152
179
  hydra.queue(request)
153
180
  end
154
181
 
155
- pending.each { |ip| enqueue.call(ip) }
182
+ chunks.each { |chunk| enqueue.call(chunk) }
156
183
  hydra.run
157
184
  end
158
185
 
159
- def settle(ip, response, attempts, retries, enqueue)
160
- result = Transport.lookup_result(response)
161
- @cache&.set(ip, result)
162
- result
186
+ # One POST /batch, mapped back onto the addresses it was asked about. A
187
+ # chunk-level failure - the call refused, the transport failing, the retries
188
+ # exhausted - becomes every address's error, exactly as it would have been
189
+ # had each been looked up alone.
190
+ def settle(chunk, response, attempts, retries, enqueue)
191
+ body = Transport.batch_body(response)
192
+ chunk.to_h do |ip|
193
+ answer = batch_answer(ip, body)
194
+ @cache&.set(ip, answer) if answer.is_a?(Result)
195
+ [ip, answer]
196
+ end
163
197
  rescue Error => e
164
- return e unless e.retryable? && attempts[ip] < retries
198
+ return chunk.to_h { |ip| [ip, e] } unless e.retryable? && attempts[chunk] < retries
165
199
 
166
- attempts[ip] += 1
200
+ attempts[chunk] += 1
167
201
  # Sleeping here stalls the whole hydra, which is what a server-supplied
168
202
  # delay asks for: it is telling every request to this host to back off.
169
- sleep(Retries.delay_for(e, attempts[ip]))
170
- enqueue.call(ip)
203
+ sleep(Retries.delay_for(e, attempts[chunk]))
204
+ enqueue.call(chunk)
171
205
  nil
172
206
  end
207
+
208
+ # Every address lands in exactly one of `results` and `errors`; an address in
209
+ # neither is the server breaking its own contract, and is reported as such
210
+ # rather than lost.
211
+ def batch_answer(ip, body)
212
+ if (served = body['results'][ip])
213
+ Result.new(served)
214
+ elsif (failed = body['errors'][ip])
215
+ Error.from_entry(failed['status'], failed['error'])
216
+ else
217
+ Error.new(:server_error, "the batch answer did not include #{ip}", status: 200)
218
+ end
219
+ end
173
220
  end
174
221
  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
@@ -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
  #