vpndetection 5.9.0 → 5.10.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -6
  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 +1 -1
  6. data/lib/vpndetection/api/database_wire_api.rb +1 -1
  7. data/lib/vpndetection/api/entitlement_wire_api.rb +1 -1
  8. data/lib/vpndetection/api/lookup_wire_api.rb +5 -5
  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/bogon.rb +4 -1
  14. data/lib/vpndetection/client.rb +2 -2
  15. data/lib/vpndetection/configuration.rb +1 -1
  16. data/lib/vpndetection/models/account_create_apikey_request.rb +1 -1
  17. data/lib/vpndetection/models/account_created_apikey.rb +1 -1
  18. data/lib/vpndetection/models/account_org.rb +1 -1
  19. data/lib/vpndetection/models/account_org_ref.rb +1 -1
  20. data/lib/vpndetection/models/account_org_wrap.rb +1 -1
  21. data/lib/vpndetection/models/account_rc.rb +1 -1
  22. data/lib/vpndetection/models/account_revealed_apikey.rb +1 -1
  23. data/lib/vpndetection/models/account_user.rb +1 -1
  24. data/lib/vpndetection/models/apikey_detail.rb +1 -1
  25. data/lib/vpndetection/models/apikey_list.rb +1 -1
  26. data/lib/vpndetection/models/asn.rb +274 -0
  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/client_registration.rb +1 -1
  32. data/lib/vpndetection/models/client_registration_request.rb +1 -1
  33. data/lib/vpndetection/models/database.rb +29 -2
  34. data/lib/vpndetection/models/database_checksums_response.rb +1 -1
  35. data/lib/vpndetection/models/database_format.rb +1 -1
  36. data/lib/vpndetection/models/database_format_size.rb +1 -1
  37. data/lib/vpndetection/models/database_list.rb +1 -1
  38. data/lib/vpndetection/models/database_metadata.rb +1 -1
  39. data/lib/vpndetection/models/database_metadata_column.rb +1 -1
  40. data/lib/vpndetection/models/database_version.rb +1 -1
  41. data/lib/vpndetection/models/db_checksums.rb +1 -1
  42. data/lib/vpndetection/models/device_authorization.rb +1 -1
  43. data/lib/vpndetection/models/download.rb +29 -2
  44. data/lib/vpndetection/models/download_list.rb +1 -1
  45. data/lib/vpndetection/models/entitlement.rb +1 -1
  46. data/lib/vpndetection/models/entitlement_apikey.rb +1 -1
  47. data/lib/vpndetection/models/entitlement_error.rb +1 -1
  48. data/lib/vpndetection/models/entitlement_plan.rb +1 -1
  49. data/lib/vpndetection/models/entitlement_usage.rb +1 -1
  50. data/lib/vpndetection/models/error_envelope.rb +1 -1
  51. data/lib/vpndetection/models/identity.rb +1 -1
  52. data/lib/vpndetection/models/location.rb +409 -0
  53. data/lib/vpndetection/models/lookup_error.rb +1 -1
  54. data/lib/vpndetection/models/lookup_response.rb +40 -22
  55. data/lib/vpndetection/models/oauth_error.rb +1 -1
  56. data/lib/vpndetection/models/oauth_metadata.rb +1 -1
  57. data/lib/vpndetection/models/proxy_detail.rb +1 -1
  58. data/lib/vpndetection/models/pubproxy_detail.rb +1 -1
  59. data/lib/vpndetection/models/standing.rb +1 -1
  60. data/lib/vpndetection/models/token_response.rb +1 -1
  61. data/lib/vpndetection/models/vpn_detail.rb +4 -4
  62. data/lib/vpndetection/result.rb +41 -17
  63. data/lib/vpndetection/transport.rb +103 -9
  64. data/lib/vpndetection/version.rb +1 -1
  65. metadata +3 -1
@@ -24,6 +24,15 @@ module VPNDetection
24
24
 
25
25
  DETAILS = %w[vpn hosting relay tor cdn resproxy dcproxy mobproxy pubproxy].freeze
26
26
 
27
+ # Where the address is, and the AS announcing it: every keyed answer carries
28
+ # both, each member present, an empty string or AS 0 where nothing is known.
29
+ LOCATION = %w[
30
+ continent continent_name country country_name timezone currency_code currency_name currency_symbol
31
+ flag_emoji flag_unicode
32
+ ].freeze
33
+
34
+ ASN = %w[asn name domain type prefix].freeze
35
+
27
36
  # The response exactly as it came off the wire, with its original names.
28
37
  # Frozen, because the cache hands this same object to every later caller of
29
38
  # the address.
@@ -66,7 +75,7 @@ module VPNDetection
66
75
  end
67
76
 
68
77
  # Whether the address belongs to a hosting or cloud provider, or nil when
69
- # your plan does not include it. Starter and above.
78
+ # your plan does not include it. Every paid plan.
70
79
  def is_hosting
71
80
  @raw['is_hosting']
72
81
  end
@@ -77,7 +86,7 @@ module VPNDetection
77
86
  end
78
87
 
79
88
  # Whether the address is a privacy relay egress, or nil when your plan does
80
- # not include it. Starter and above.
89
+ # not include it. Free and paid plans.
81
90
  def is_relay
82
91
  @raw['is_relay']
83
92
  end
@@ -88,7 +97,7 @@ module VPNDetection
88
97
  end
89
98
 
90
99
  # Whether the address is a Tor node, or nil when your plan does not include
91
- # it. Starter and above.
100
+ # it. Free and paid plans.
92
101
  def is_tor
93
102
  @raw['is_tor']
94
103
  end
@@ -99,7 +108,7 @@ module VPNDetection
99
108
  end
100
109
 
101
110
  # Whether the address belongs to a CDN, or nil when your plan does not
102
- # include it. Starter and above.
111
+ # include it. Every paid plan.
103
112
  def is_cdn
104
113
  @raw['is_cdn']
105
114
  end
@@ -110,7 +119,7 @@ module VPNDetection
110
119
  end
111
120
 
112
121
  # Whether the address was seen in a residential proxy pool, or nil when your
113
- # plan does not include it. Scale and above.
122
+ # plan does not include it. Every paid plan.
114
123
  def is_resproxy
115
124
  @raw['is_resproxy']
116
125
  end
@@ -121,7 +130,7 @@ module VPNDetection
121
130
  end
122
131
 
123
132
  # Whether the address was seen in a datacenter proxy pool, or nil when your
124
- # plan does not include it. Scale and above.
133
+ # plan does not include it. Every paid plan.
125
134
  def is_dcproxy
126
135
  @raw['is_dcproxy']
127
136
  end
@@ -132,7 +141,7 @@ module VPNDetection
132
141
  end
133
142
 
134
143
  # Whether the address was seen in a mobile proxy pool, or nil when your plan
135
- # does not include it. Scale and above.
144
+ # does not include it. Every paid plan.
136
145
  def is_mobproxy
137
146
  @raw['is_mobproxy']
138
147
  end
@@ -144,7 +153,7 @@ module VPNDetection
144
153
 
145
154
  # Whether a public proxy, one anyone can connect through, listens on the
146
155
  # address or sends its traffic out from it, or nil when your plan does not
147
- # include it. Scale and above.
156
+ # include it. Every paid plan.
148
157
  def is_pubproxy
149
158
  @raw['is_pubproxy']
150
159
  end
@@ -154,42 +163,43 @@ module VPNDetection
154
163
  @raw['is_pubproxy'] == true
155
164
  end
156
165
 
157
- # Detail for `is_vpn`: provider, last_seen, and on max confidence and method.
166
+ # Detail for `is_vpn`: provider, last_seen and confidence. Every paid plan.
167
+ # `method` is deprecated: no plan is answered with it since 2026-10-20.
158
168
  def vpn
159
169
  @raw['vpn']
160
170
  end
161
171
 
162
- # Detail for `is_hosting`. Scale and above.
172
+ # Detail for `is_hosting`. Every paid plan.
163
173
  def hosting
164
174
  @raw['hosting']
165
175
  end
166
176
 
167
- # Detail for `is_relay`. Scale and above.
177
+ # Detail for `is_relay`. Free and paid plans.
168
178
  def relay
169
179
  @raw['relay']
170
180
  end
171
181
 
172
- # Detail for `is_tor`. Scale and above.
182
+ # Detail for `is_tor`. Free and paid plans.
173
183
  def tor
174
184
  @raw['tor']
175
185
  end
176
186
 
177
- # Detail for `is_cdn`. Scale and above.
187
+ # Detail for `is_cdn`. Every paid plan.
178
188
  def cdn
179
189
  @raw['cdn']
180
190
  end
181
191
 
182
- # Detail for `is_resproxy`. Max only.
192
+ # Detail for `is_resproxy`. Every paid plan.
183
193
  def resproxy
184
194
  @raw['resproxy']
185
195
  end
186
196
 
187
- # Detail for `is_dcproxy`. Max only.
197
+ # Detail for `is_dcproxy`. Every paid plan.
188
198
  def dcproxy
189
199
  @raw['dcproxy']
190
200
  end
191
201
 
192
- # Detail for `is_mobproxy`. Max only.
202
+ # Detail for `is_mobproxy`. Every paid plan.
193
203
  def mobproxy
194
204
  @raw['mobproxy']
195
205
  end
@@ -198,11 +208,25 @@ module VPNDetection
198
208
  # num_proxies, last_seen and last_usable. num_ports is 0 when the address
199
209
  # is only an exit and num_proxies 0 when it is only an entry; last_usable is
200
210
  # an empty string when no request through a proxy on the address was
201
- # answered. Max only.
211
+ # answered. Every paid plan.
202
212
  def pubproxy
203
213
  @raw['pubproxy']
204
214
  end
205
215
 
216
+ # Where the address is: continent, country, timezone, the country's
217
+ # currency and flag, as a wire hash (`result.location['country']`). Free and
218
+ # paid plans.
219
+ def location
220
+ @raw['location']
221
+ end
222
+
223
+ # The AS announcing the address: asn, name, domain, type and prefix, as a
224
+ # wire hash (`result.asn['name']`), with asn 0 where none does. Free and paid
225
+ # plans.
226
+ def asn
227
+ @raw['asn']
228
+ end
229
+
206
230
  def to_h
207
231
  @raw
208
232
  end
@@ -86,11 +86,18 @@ module VPNDetection
86
86
  # the spec requires. The generated reader raised a RuntimeError, a
87
87
  # JSON::ParserError or an ArgumentError for those, none of them retried, and
88
88
  # handed an empty or non-object body back as nil, as if it were the answer.
89
+ #
90
+ # Every member present is checked against the type the spec gives it, at any
91
+ # depth, before the answer is handed back: the generated reader converts with
92
+ # `to_i`, so `"version":"x"` came back as 0 and `"version":"3"` as 3, and it
93
+ # skips a list member sent as null, so `"versions": null` came back as nil
94
+ # (5.9.0, measured 2026-10-20).
89
95
  def deserialize(response, return_type)
90
96
  data = super
91
- return data unless data.nil?
97
+ raise Error.new(:server_error, "the API answered with no #{return_type}", status: response.code) if data.nil?
92
98
 
93
- raise Error.new(:server_error, "the API answered with no #{return_type}", status: response.code)
99
+ Transport.check_type!(JSON.parse(response.body.to_s), return_type.to_s, 'the answer')
100
+ data
94
101
  rescue Error
95
102
  raise
96
103
  rescue StandardError => e
@@ -182,6 +189,7 @@ module VPNDetection
182
189
 
183
190
  body = parse_object(response)
184
191
  begin
192
+ check_type!(body, 'Entitlement', 'the answer')
185
193
  Entitlement.build_from_hash(body)
186
194
  rescue StandardError => e
187
195
  raise Error.new(:server_error, "could not read the answer as Entitlement: #{e.message}",
@@ -194,15 +202,21 @@ module VPNDetection
194
202
  raise Error.from_status(response.code, response.headers, response.body) unless response.success?
195
203
 
196
204
  body = parse_object(response)
197
- return Result.new(body) if lookup_answer?(body)
198
-
199
- raise Error.new(:server_error, 'the answer carried no ip and is_vpn', status: response.code)
205
+ begin
206
+ check_type!(body, 'LookupResponse', 'the answer')
207
+ rescue TypeError => e
208
+ raise Error.new(:server_error, "could not read the answer as LookupResponse: #{e.message}",
209
+ status: response.code)
210
+ end
211
+ Result.new(body)
200
212
  end
201
213
 
202
- # Whether a parsed body is a lookup's answer: every plan's carries `ip` and
203
- # `is_vpn`, so one without them is a 2xx the call cannot read.
204
- def self.lookup_answer?(body)
205
- body.is_a?(Hash) && body['ip'].is_a?(String) && [true, false].include?(body['is_vpn'])
214
+ # Whether a parsed value is the generated `type`: a batch entry that is not
215
+ # is that address's error alone.
216
+ def self.conforms?(value, type)
217
+ check_type!(value, type, 'the entry')
218
+ rescue TypeError
219
+ false
206
220
  end
207
221
 
208
222
  # The two maps of a batch answer, each present even when empty. An answer
@@ -269,5 +283,85 @@ module VPNDetection
269
283
  end
270
284
 
271
285
  private_class_method :transport_failure?, :parse_object, :oauth_refusal
286
+
287
+ # The members the spec requires of each answer a lookup, a batch, the
288
+ # entitlement or a database call reads. One that is not nullable must be
289
+ # there and not null. Every plan's lookup carries `ip` and `is_vpn`, and a
290
+ # detail's members are as optional as the plan makes them.
291
+ # test/client_test.rb holds this to spec/openapi.yaml.
292
+ REQUIRED = {
293
+ 'LookupResponse' => %i[ip is_vpn],
294
+ 'VpnDetail' => [],
295
+ 'ClassDetail' => [],
296
+ 'ProxyDetail' => [],
297
+ 'PubproxyDetail' => [],
298
+ 'Location' => %i[continent continent_name country country_name timezone currency_code currency_name
299
+ currency_symbol flag_emoji flag_unicode],
300
+ 'Asn' => %i[asn name domain type prefix],
301
+ 'BatchLookupError' => %i[status error],
302
+ 'Entitlement' => %i[org_id apikey plan usage],
303
+ 'EntitlementApikey' => %i[id expires allowed_cidrs],
304
+ 'EntitlementPlan' => %i[key tier],
305
+ 'EntitlementUsage' => %i[requests quota hard_limit window_start window_end],
306
+ 'DatabaseList' => %i[databases],
307
+ 'Database' => %i[base name summary license_type starts expires renews_at notice_due_at in_term standing
308
+ open versions],
309
+ 'DatabaseVersion' => %i[id version formats],
310
+ 'DatabaseFormatSize' => %i[format bytes],
311
+ 'DatabaseMetadata' => %i[id updated entries schema],
312
+ 'DatabaseMetadataColumn' => %i[name type],
313
+ 'DatabaseChecksumsResponse' => %i[id format checksums],
314
+ 'DbChecksums' => %i[md5 sha1 sha256 sha512],
315
+ 'DownloadList' => %i[downloads],
316
+ 'Download' => %i[dataset_id format outcome sample open bytes http_status apikey_id client_ip user_agent
317
+ created],
318
+ }.freeze
319
+
320
+ # True when the parsed `value` has the generated `type`, a model's members
321
+ # recursively; raises TypeError naming where it does not. No list entry or
322
+ # map value the spec describes may be null: through 5.9.0 one came back as
323
+ # nil, or as 0 in `size`, and a lookup's `"is_hosting": "yes"` came back as
324
+ # the answer.
325
+ def self.check_type!(value, type, where)
326
+ raise TypeError, "#{where} is null" if value.nil?
327
+
328
+ typed =
329
+ case type
330
+ when 'String', 'Date', 'Time' then value.is_a?(String)
331
+ when 'Integer' then value.is_a?(Integer)
332
+ when 'Float' then value.is_a?(Numeric)
333
+ when 'Boolean' then [true, false].include?(value)
334
+ when 'Object' then true
335
+ when /\AArray<(?<inner>.+)>\z/
336
+ inner = Regexp.last_match(:inner)
337
+ value.is_a?(Array) && value.each_with_index.all? { |v, i| check_type!(v, inner, "#{where}[#{i}]") }
338
+ when /\AHash<String, ?(?<inner>.+)>\z/
339
+ inner = Regexp.last_match(:inner)
340
+ value.is_a?(Hash) && value.all? { |k, v| check_type!(v, inner, "#{where}.#{k}") }
341
+ else
342
+ model = VPNDetection.const_get(type)
343
+ if model.respond_to?(:openapi_types)
344
+ value.is_a?(Hash) && model.openapi_types.all? { |attr, member| check_member!(model, attr, member, value, where) }
345
+ else
346
+ value.is_a?(String) # an enum
347
+ end
348
+ end
349
+ return true if typed
350
+
351
+ raise TypeError, "#{where} is not a #{type}"
352
+ end
353
+
354
+ # A member absent or null reads as absent, unless the spec requires it and
355
+ # does not make it nullable.
356
+ def self.check_member!(model, attr, member, value, where)
357
+ key = model.attribute_map[attr].to_s
358
+ return check_type!(value[key], member.to_s, "#{where}.#{key}") unless value[key].nil?
359
+ return true unless REQUIRED.fetch(model.name.split('::').last).include?(attr)
360
+ return true if model.openapi_nullable.include?(attr)
361
+
362
+ raise TypeError, "#{where}.#{key} is #{value.key?(key) ? 'null' : 'missing'}"
363
+ end
364
+
365
+ private_class_method :check_member!
272
366
  end
273
367
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module VPNDetection
4
- VERSION = '5.9.0'
4
+ VERSION = '5.10.0'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vpndetection
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.9.0
4
+ version: 5.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mslm Dev
@@ -78,6 +78,7 @@ files:
78
78
  - lib/vpndetection/models/account_user.rb
79
79
  - lib/vpndetection/models/apikey_detail.rb
80
80
  - lib/vpndetection/models/apikey_list.rb
81
+ - lib/vpndetection/models/asn.rb
81
82
  - lib/vpndetection/models/batch_lookup_error.rb
82
83
  - lib/vpndetection/models/batch_lookup_request.rb
83
84
  - lib/vpndetection/models/batch_lookup_response.rb
@@ -103,6 +104,7 @@ files:
103
104
  - lib/vpndetection/models/entitlement_usage.rb
104
105
  - lib/vpndetection/models/error_envelope.rb
105
106
  - lib/vpndetection/models/identity.rb
107
+ - lib/vpndetection/models/location.rb
106
108
  - lib/vpndetection/models/lookup_error.rb
107
109
  - lib/vpndetection/models/lookup_response.rb
108
110
  - lib/vpndetection/models/oauth_error.rb