verifip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +151 -0
- data/lib/verifip/client.rb +196 -0
- data/lib/verifip/errors.rb +38 -0
- data/lib/verifip/models.rb +162 -0
- data/lib/verifip/version.rb +5 -0
- data/lib/verifip.rb +17 -0
- data/verifip.gemspec +29 -0
- metadata +82 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: dacef2f215d3e573f691c0919822285a87362ad9dad135d2976669b2e7585c8d
|
|
4
|
+
data.tar.gz: 17051feb5eb6c39f5c7a8a2bfafaac1ea6fa9ee31279c7f51a593f8c922a8dcf
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5c9314aac5692163a7d465eda51e45dc46c7751b6044f4a4fcf1ce8205ca044819cbcc19ae9a4273dbba06aace7b66b6ba471d4ade68071cf20a0eb66cf35185
|
|
7
|
+
data.tar.gz: 56b179f20e5ad461f7bf2641a6d6535be366f97f11481db5a658d503efc7167054fa196322a74b76aab48b9802c95a26b148989f0cedd05f2158b5f11b0467e9
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Qubit HQ
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# VerifIP Ruby SDK
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [VerifIP](https://verifip.com) IP fraud scoring API.
|
|
4
|
+
|
|
5
|
+
Requires **Ruby 3.1+**. Zero runtime dependencies -- uses only Ruby stdlib (`net/http`, `json`, `uri`).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Gemfile
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem "verifip", "~> 0.1.0"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Manual
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
gem install verifip
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
require "verifip"
|
|
25
|
+
|
|
26
|
+
client = VerifIP::Client.new(api_key: "vip_your_key")
|
|
27
|
+
result = client.check("185.220.101.1")
|
|
28
|
+
|
|
29
|
+
puts result.fraud_score # 0-100
|
|
30
|
+
puts result.vpn? # true/false
|
|
31
|
+
puts result.country_code # "DE"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Configuration
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
client = VerifIP::Client.new(
|
|
38
|
+
api_key: "vip_your_key",
|
|
39
|
+
base_url: "https://api.verifip.com", # optional
|
|
40
|
+
timeout: 15, # seconds, default: 30
|
|
41
|
+
max_retries: 5 # default: 3
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Methods
|
|
46
|
+
|
|
47
|
+
### `check(ip)` -- Single IP Check
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
result = client.check("185.220.101.1")
|
|
51
|
+
|
|
52
|
+
result.request_id # unique request ID
|
|
53
|
+
result.ip # queried IP
|
|
54
|
+
result.fraud_score # 0-100 risk score
|
|
55
|
+
result.is_proxy # proxy detected
|
|
56
|
+
result.proxy? # alias for is_proxy
|
|
57
|
+
result.is_vpn # VPN detected
|
|
58
|
+
result.vpn? # alias for is_vpn
|
|
59
|
+
result.is_tor # Tor exit node
|
|
60
|
+
result.tor? # alias for is_tor
|
|
61
|
+
result.is_datacenter # datacenter IP
|
|
62
|
+
result.datacenter? # alias for is_datacenter
|
|
63
|
+
result.country_code # "US", "DE", etc.
|
|
64
|
+
result.country_name # "United States", etc.
|
|
65
|
+
result.region # state/province
|
|
66
|
+
result.city # city name
|
|
67
|
+
result.isp # ISP name
|
|
68
|
+
result.asn # AS number
|
|
69
|
+
result.connection_type # "residential", "datacenter", etc.
|
|
70
|
+
result.hostname # reverse DNS hostname
|
|
71
|
+
result.signal_breakdown # Hash of signal name => score
|
|
72
|
+
result.error # error message if check failed, nil otherwise
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### `check_batch(ips)` -- Batch Check
|
|
76
|
+
|
|
77
|
+
Check up to 100 IPs in a single request (Starter plan or higher):
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
batch = client.check_batch(["8.8.8.8", "1.1.1.1", "185.220.101.1"])
|
|
81
|
+
|
|
82
|
+
batch.results.each do |r|
|
|
83
|
+
puts "#{r.ip} -> score=#{r.fraud_score}"
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `health` -- Health Check
|
|
88
|
+
|
|
89
|
+
Check API server status (no authentication required):
|
|
90
|
+
|
|
91
|
+
```ruby
|
|
92
|
+
health = client.health
|
|
93
|
+
puts health.status # "ok"
|
|
94
|
+
puts health.version # API version
|
|
95
|
+
puts health.uptime_seconds # server uptime
|
|
96
|
+
puts health.redis # "connected"
|
|
97
|
+
puts health.postgres # "connected"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Rate Limit Info
|
|
101
|
+
|
|
102
|
+
After any API call, inspect the most recent rate limit headers:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
info = client.rate_limit_info
|
|
106
|
+
if info
|
|
107
|
+
puts info.limit # max requests
|
|
108
|
+
puts info.remaining # remaining
|
|
109
|
+
puts info.reset # Time object
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Error Handling
|
|
114
|
+
|
|
115
|
+
All errors inherit from `VerifIP::VerifIPError` (which inherits from `StandardError`):
|
|
116
|
+
|
|
117
|
+
| Error | HTTP Status | When |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `AuthenticationError` | 401, 403 | Invalid or disabled API key |
|
|
120
|
+
| `RateLimitError` | 429 | Daily request limit exceeded |
|
|
121
|
+
| `InvalidRequestError` | 400 | Malformed IP, bad request body |
|
|
122
|
+
| `ServerError` | 5xx | Server-side errors |
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
begin
|
|
126
|
+
result = client.check("185.220.101.1")
|
|
127
|
+
rescue VerifIP::AuthenticationError => e
|
|
128
|
+
puts "Bad API key: #{e.message}"
|
|
129
|
+
rescue VerifIP::RateLimitError => e
|
|
130
|
+
puts "Rate limited, retry after: #{e.retry_after}s"
|
|
131
|
+
rescue VerifIP::InvalidRequestError => e
|
|
132
|
+
puts "Bad request: #{e.message}"
|
|
133
|
+
rescue VerifIP::ServerError => e
|
|
134
|
+
puts "Server error: #{e.status_code}"
|
|
135
|
+
rescue VerifIP::VerifIPError => e
|
|
136
|
+
puts "API error: #{e.message}"
|
|
137
|
+
end
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
All errors expose:
|
|
141
|
+
- `status_code` -- HTTP status code (0 for connection errors)
|
|
142
|
+
- `error_code` -- machine-readable error code
|
|
143
|
+
- `retry_after` -- seconds to wait before retrying (may be nil)
|
|
144
|
+
|
|
145
|
+
## Automatic Retries
|
|
146
|
+
|
|
147
|
+
The SDK automatically retries on 429 and 5xx errors with exponential backoff and jitter. Configure with `max_retries:` (default 3, set to 0 to disable).
|
|
148
|
+
|
|
149
|
+
## License
|
|
150
|
+
|
|
151
|
+
MIT
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module VerifIP
|
|
8
|
+
# Client for the VerifIP IP fraud scoring API.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# client = VerifIP::Client.new(api_key: "vip_your_key")
|
|
12
|
+
# result = client.check("185.220.101.1")
|
|
13
|
+
# puts result.fraud_score # => 70
|
|
14
|
+
#
|
|
15
|
+
class Client
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.verifip.com"
|
|
17
|
+
DEFAULT_TIMEOUT = 30
|
|
18
|
+
DEFAULT_MAX_RETRIES = 3
|
|
19
|
+
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
20
|
+
USER_AGENT = "verifip-ruby/#{VerifIP::VERSION}"
|
|
21
|
+
|
|
22
|
+
# @return [RateLimitInfo, nil] most recently observed rate limit info
|
|
23
|
+
def rate_limit_info
|
|
24
|
+
@mutex.synchronize { @rate_limit_info }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Create a new VerifIP client.
|
|
28
|
+
#
|
|
29
|
+
# @param api_key [String] your VerifIP API key (starts with "vip_")
|
|
30
|
+
# @param base_url [String] API base URL (default: https://api.verifip.com)
|
|
31
|
+
# @param timeout [Integer] request timeout in seconds (default: 30)
|
|
32
|
+
# @param max_retries [Integer] max retry attempts on 429/5xx (default: 3)
|
|
33
|
+
def initialize(api_key:, base_url: DEFAULT_BASE_URL, timeout: DEFAULT_TIMEOUT, max_retries: DEFAULT_MAX_RETRIES)
|
|
34
|
+
raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
|
|
35
|
+
|
|
36
|
+
@api_key = api_key
|
|
37
|
+
@base_url = base_url.chomp("/")
|
|
38
|
+
@timeout = timeout
|
|
39
|
+
@max_retries = max_retries
|
|
40
|
+
@rate_limit_info = nil
|
|
41
|
+
@mutex = Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Check a single IP address for fraud risk.
|
|
45
|
+
#
|
|
46
|
+
# @param ip [String] IPv4 or IPv6 address
|
|
47
|
+
# @return [CheckResponse]
|
|
48
|
+
# @raise [InvalidRequestError] if the IP is malformed or reserved
|
|
49
|
+
# @raise [AuthenticationError] if the API key is invalid or disabled
|
|
50
|
+
# @raise [RateLimitError] if the daily limit is exceeded
|
|
51
|
+
def check(ip)
|
|
52
|
+
raise ArgumentError, "ip is required" if ip.nil? || ip.empty?
|
|
53
|
+
|
|
54
|
+
data = request(:get, "/v1/check?ip=#{URI.encode_uri_component(ip)}", auth: true)
|
|
55
|
+
CheckResponse.from_hash(data)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Check multiple IP addresses in a single request.
|
|
59
|
+
#
|
|
60
|
+
# Requires Starter plan or higher. Maximum 100 IPs per request.
|
|
61
|
+
#
|
|
62
|
+
# @param ips [Array<String>] list of IPv4/IPv6 addresses (1-100)
|
|
63
|
+
# @return [BatchResponse]
|
|
64
|
+
def check_batch(ips)
|
|
65
|
+
raise ArgumentError, "ips list is required and cannot be empty" if ips.nil? || ips.empty?
|
|
66
|
+
raise ArgumentError, "Maximum 100 IPs per batch request" if ips.size > 100
|
|
67
|
+
|
|
68
|
+
body = JSON.generate({ ips: ips })
|
|
69
|
+
data = request(:post, "/v1/check/batch", body: body, auth: true)
|
|
70
|
+
BatchResponse.from_hash(data)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Check API server health status. Does not require authentication.
|
|
74
|
+
#
|
|
75
|
+
# @return [HealthResponse]
|
|
76
|
+
def health
|
|
77
|
+
data = request(:get, "/health", auth: false)
|
|
78
|
+
HealthResponse.from_hash(data)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def to_s = "VerifIP::Client(base_url=#{@base_url})"
|
|
82
|
+
def inspect = to_s
|
|
83
|
+
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def request(method, path, body: nil, auth: true)
|
|
87
|
+
uri = URI("#{@base_url}#{path}")
|
|
88
|
+
last_error = nil
|
|
89
|
+
|
|
90
|
+
(@max_retries + 1).times do |attempt|
|
|
91
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
92
|
+
http.use_ssl = (uri.scheme == "https")
|
|
93
|
+
http.open_timeout = @timeout
|
|
94
|
+
http.read_timeout = @timeout
|
|
95
|
+
|
|
96
|
+
req = build_request(method, uri, body: body, auth: auth)
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
response = http.request(req)
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
last_error = VerifIPError.new(
|
|
102
|
+
"Connection error: #{e.message}",
|
|
103
|
+
status_code: 0,
|
|
104
|
+
error_code: "connection_error"
|
|
105
|
+
)
|
|
106
|
+
if attempt < @max_retries
|
|
107
|
+
sleep(backoff_delay(attempt))
|
|
108
|
+
next
|
|
109
|
+
end
|
|
110
|
+
raise last_error
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
status = response.code.to_i
|
|
114
|
+
update_rate_limit(response)
|
|
115
|
+
|
|
116
|
+
if status >= 200 && status < 300
|
|
117
|
+
resp_body = response.body
|
|
118
|
+
return (resp_body && !resp_body.empty?) ? JSON.parse(resp_body) : {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Parse error response
|
|
122
|
+
error_data = begin
|
|
123
|
+
JSON.parse(response.body || "")
|
|
124
|
+
rescue JSON::ParserError
|
|
125
|
+
{}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
error_code = error_data["error"] || ""
|
|
129
|
+
message = error_data["message"] || response.body || ""
|
|
130
|
+
retry_after = error_data["retry_after"]
|
|
131
|
+
|
|
132
|
+
err = make_error(status, error_code, message, retry_after)
|
|
133
|
+
|
|
134
|
+
if RETRYABLE_STATUSES.include?(status) && attempt < @max_retries
|
|
135
|
+
last_error = err
|
|
136
|
+
delay = retry_after || (0.5 * (2**attempt))
|
|
137
|
+
delay = [delay, 30].min
|
|
138
|
+
delay += rand * 0.25 * delay
|
|
139
|
+
sleep(delay)
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise err
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
raise last_error || VerifIPError.new("Request failed after retries")
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def build_request(method, uri, body: nil, auth: true)
|
|
150
|
+
req = case method
|
|
151
|
+
when :get
|
|
152
|
+
Net::HTTP::Get.new(uri)
|
|
153
|
+
when :post
|
|
154
|
+
Net::HTTP::Post.new(uri)
|
|
155
|
+
else
|
|
156
|
+
raise ArgumentError, "Unsupported method: #{method}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
req["User-Agent"] = USER_AGENT
|
|
160
|
+
req["Accept"] = "application/json"
|
|
161
|
+
req["Authorization"] = "Bearer #{@api_key}" if auth
|
|
162
|
+
|
|
163
|
+
if body
|
|
164
|
+
req["Content-Type"] = "application/json"
|
|
165
|
+
req.body = body
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
req
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def update_rate_limit(response)
|
|
172
|
+
info = RateLimitInfo.from_headers(response)
|
|
173
|
+
@mutex.synchronize { @rate_limit_info = info } if info
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def make_error(status, code, message, retry_after)
|
|
177
|
+
kwargs = { status_code: status, error_code: code, retry_after: retry_after }
|
|
178
|
+
case status
|
|
179
|
+
when 400
|
|
180
|
+
InvalidRequestError.new(message, **kwargs)
|
|
181
|
+
when 401, 403
|
|
182
|
+
AuthenticationError.new(message, **kwargs)
|
|
183
|
+
when 429
|
|
184
|
+
RateLimitError.new(message, **kwargs)
|
|
185
|
+
when 500..599
|
|
186
|
+
ServerError.new(message, **kwargs)
|
|
187
|
+
else
|
|
188
|
+
VerifIPError.new(message, **kwargs)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def backoff_delay(attempt)
|
|
193
|
+
0.5 * (2**attempt)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifIP
|
|
4
|
+
# Base error class for all VerifIP API errors.
|
|
5
|
+
class VerifIPError < StandardError
|
|
6
|
+
# @return [Integer] HTTP status code (0 for connection errors)
|
|
7
|
+
attr_reader :status_code
|
|
8
|
+
|
|
9
|
+
# @return [String] machine-readable error code from the API
|
|
10
|
+
attr_reader :error_code
|
|
11
|
+
|
|
12
|
+
# @return [Integer, nil] suggested seconds to wait before retrying
|
|
13
|
+
attr_reader :retry_after
|
|
14
|
+
|
|
15
|
+
def initialize(message = nil, status_code: 0, error_code: "", retry_after: nil)
|
|
16
|
+
super(message)
|
|
17
|
+
@status_code = status_code
|
|
18
|
+
@error_code = error_code || ""
|
|
19
|
+
@retry_after = retry_after
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
"#{self.class.name}(#{status_code}, '#{error_code}', '#{message}')"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Raised on 401 (invalid API key) or 403 (key disabled).
|
|
28
|
+
class AuthenticationError < VerifIPError; end
|
|
29
|
+
|
|
30
|
+
# Raised on 429 (rate limit exceeded). Check {#retry_after} for wait time.
|
|
31
|
+
class RateLimitError < VerifIPError; end
|
|
32
|
+
|
|
33
|
+
# Raised on 400 (invalid IP, bad request body).
|
|
34
|
+
class InvalidRequestError < VerifIPError; end
|
|
35
|
+
|
|
36
|
+
# Raised on 5xx server errors.
|
|
37
|
+
class ServerError < VerifIPError; end
|
|
38
|
+
end
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module VerifIP
|
|
4
|
+
# Response from a single IP check containing fraud score, threat flags,
|
|
5
|
+
# geolocation data, and signal breakdown.
|
|
6
|
+
class CheckResponse
|
|
7
|
+
FIELDS = %i[
|
|
8
|
+
request_id ip fraud_score is_proxy is_vpn is_tor is_datacenter
|
|
9
|
+
country_code country_name region city isp asn connection_type
|
|
10
|
+
hostname signal_breakdown error
|
|
11
|
+
].freeze
|
|
12
|
+
|
|
13
|
+
attr_reader(*FIELDS)
|
|
14
|
+
|
|
15
|
+
def initialize(**kwargs)
|
|
16
|
+
@request_id = kwargs.fetch(:request_id, "")
|
|
17
|
+
@ip = kwargs.fetch(:ip, "")
|
|
18
|
+
@fraud_score = kwargs.fetch(:fraud_score, 0)
|
|
19
|
+
@is_proxy = kwargs.fetch(:is_proxy, false)
|
|
20
|
+
@is_vpn = kwargs.fetch(:is_vpn, false)
|
|
21
|
+
@is_tor = kwargs.fetch(:is_tor, false)
|
|
22
|
+
@is_datacenter = kwargs.fetch(:is_datacenter, false)
|
|
23
|
+
@country_code = kwargs.fetch(:country_code, "")
|
|
24
|
+
@country_name = kwargs.fetch(:country_name, "")
|
|
25
|
+
@region = kwargs.fetch(:region, "")
|
|
26
|
+
@city = kwargs.fetch(:city, "")
|
|
27
|
+
@isp = kwargs.fetch(:isp, "")
|
|
28
|
+
@asn = kwargs.fetch(:asn, 0)
|
|
29
|
+
@connection_type = kwargs.fetch(:connection_type, "")
|
|
30
|
+
@hostname = kwargs.fetch(:hostname, "")
|
|
31
|
+
@signal_breakdown = kwargs.fetch(:signal_breakdown, {})
|
|
32
|
+
@error = kwargs.fetch(:error, nil)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Build a CheckResponse from a parsed JSON hash.
|
|
36
|
+
#
|
|
37
|
+
# @param hash [Hash] parsed API response
|
|
38
|
+
# @return [CheckResponse]
|
|
39
|
+
def self.from_hash(hash)
|
|
40
|
+
hash = _symbolize(hash)
|
|
41
|
+
new(**hash.slice(*FIELDS))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def proxy? = @is_proxy
|
|
45
|
+
def vpn? = @is_vpn
|
|
46
|
+
def tor? = @is_tor
|
|
47
|
+
def datacenter? = @is_datacenter
|
|
48
|
+
|
|
49
|
+
def to_h
|
|
50
|
+
FIELDS.each_with_object({}) { |f, h| h[f] = send(f) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def to_s
|
|
54
|
+
"CheckResponse(ip=#{@ip}, fraud_score=#{@fraud_score}, proxy=#{@is_proxy}, vpn=#{@is_vpn})"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def inspect = to_s
|
|
58
|
+
|
|
59
|
+
# @api private
|
|
60
|
+
def self._symbolize(hash)
|
|
61
|
+
hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
62
|
+
end
|
|
63
|
+
private_class_method :_symbolize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Response from a batch IP check.
|
|
67
|
+
class BatchResponse
|
|
68
|
+
# @return [Array<CheckResponse>]
|
|
69
|
+
attr_reader :results
|
|
70
|
+
|
|
71
|
+
def initialize(results = [])
|
|
72
|
+
@results = results.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Build a BatchResponse from a parsed JSON hash.
|
|
76
|
+
#
|
|
77
|
+
# @param hash [Hash] parsed API response with "results" key
|
|
78
|
+
# @return [BatchResponse]
|
|
79
|
+
def self.from_hash(hash)
|
|
80
|
+
items = (hash["results"] || hash[:results] || []).map do |r|
|
|
81
|
+
CheckResponse.from_hash(r)
|
|
82
|
+
end
|
|
83
|
+
new(items)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def size = @results.size
|
|
87
|
+
|
|
88
|
+
def to_s = "BatchResponse(results=#{size})"
|
|
89
|
+
def inspect = to_s
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Response from the health check endpoint.
|
|
93
|
+
class HealthResponse
|
|
94
|
+
attr_reader :status, :version, :data_loaded_at, :redis, :postgres, :uptime_seconds
|
|
95
|
+
|
|
96
|
+
def initialize(**kwargs)
|
|
97
|
+
@status = kwargs.fetch(:status, "")
|
|
98
|
+
@version = kwargs.fetch(:version, "")
|
|
99
|
+
@data_loaded_at = kwargs.fetch(:data_loaded_at, "")
|
|
100
|
+
@redis = kwargs.fetch(:redis, "")
|
|
101
|
+
@postgres = kwargs.fetch(:postgres, "")
|
|
102
|
+
@uptime_seconds = kwargs.fetch(:uptime_seconds, 0)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Build a HealthResponse from a parsed JSON hash.
|
|
106
|
+
#
|
|
107
|
+
# @param hash [Hash] parsed API response
|
|
108
|
+
# @return [HealthResponse]
|
|
109
|
+
def self.from_hash(hash)
|
|
110
|
+
hash = hash.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
|
|
111
|
+
new(**hash.slice(:status, :version, :data_loaded_at, :redis, :postgres, :uptime_seconds))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def to_s = "HealthResponse(status=#{@status}, version=#{@version})"
|
|
115
|
+
def inspect = to_s
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Rate limit information parsed from response headers.
|
|
119
|
+
class RateLimitInfo
|
|
120
|
+
# @return [Integer] maximum requests in the current window
|
|
121
|
+
attr_reader :limit
|
|
122
|
+
|
|
123
|
+
# @return [Integer] remaining requests in the current window
|
|
124
|
+
attr_reader :remaining
|
|
125
|
+
|
|
126
|
+
# @return [Time, nil] time at which the window resets
|
|
127
|
+
attr_reader :reset
|
|
128
|
+
|
|
129
|
+
def initialize(limit:, remaining:, reset: nil)
|
|
130
|
+
@limit = limit
|
|
131
|
+
@remaining = remaining
|
|
132
|
+
@reset = reset
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Parse rate limit info from HTTP response headers.
|
|
136
|
+
#
|
|
137
|
+
# @param headers [Hash, Net::HTTPResponse] response headers
|
|
138
|
+
# @return [RateLimitInfo, nil] nil if rate limit headers are absent
|
|
139
|
+
def self.from_headers(headers)
|
|
140
|
+
limit_str = headers["X-RateLimit-Limit"] || headers["x-ratelimit-limit"]
|
|
141
|
+
return nil if limit_str.nil?
|
|
142
|
+
|
|
143
|
+
limit = limit_str.to_i
|
|
144
|
+
remaining = (headers["X-RateLimit-Remaining"] || headers["x-ratelimit-remaining"] || "0").to_i
|
|
145
|
+
|
|
146
|
+
reset_str = headers["X-RateLimit-Reset"] || headers["x-ratelimit-reset"]
|
|
147
|
+
reset = nil
|
|
148
|
+
if reset_str
|
|
149
|
+
begin
|
|
150
|
+
reset = Time.at(reset_str.to_i).utc
|
|
151
|
+
rescue ArgumentError, TypeError
|
|
152
|
+
# ignore
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
new(limit: limit, remaining: remaining, reset: reset)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def to_s = "RateLimitInfo(limit=#{@limit}, remaining=#{@remaining}, reset=#{@reset})"
|
|
160
|
+
def inspect = to_s
|
|
161
|
+
end
|
|
162
|
+
end
|
data/lib/verifip.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "verifip/version"
|
|
4
|
+
require_relative "verifip/errors"
|
|
5
|
+
require_relative "verifip/models"
|
|
6
|
+
require_relative "verifip/client"
|
|
7
|
+
|
|
8
|
+
module VerifIP
|
|
9
|
+
# Convenience method to create a new client.
|
|
10
|
+
#
|
|
11
|
+
# @param api_key [String] your VerifIP API key
|
|
12
|
+
# @param kwargs [Hash] additional options passed to {Client#initialize}
|
|
13
|
+
# @return [Client]
|
|
14
|
+
def self.client(api_key:, **kwargs)
|
|
15
|
+
Client.new(api_key: api_key, **kwargs)
|
|
16
|
+
end
|
|
17
|
+
end
|
data/verifip.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.name = "verifip"
|
|
3
|
+
s.version = "0.1.0"
|
|
4
|
+
s.summary = "Official Ruby SDK for the VerifIP IP fraud scoring API"
|
|
5
|
+
s.description = "Ruby client library for the VerifIP API. Check IP addresses " \
|
|
6
|
+
"for fraud risk, VPN/proxy/Tor detection, geolocation, and more."
|
|
7
|
+
s.authors = ["VerifIP"]
|
|
8
|
+
s.email = ["support@verifip.com"]
|
|
9
|
+
s.homepage = "https://github.com/verifip/verifip-ruby"
|
|
10
|
+
s.license = "MIT"
|
|
11
|
+
|
|
12
|
+
s.required_ruby_version = ">= 3.1.0"
|
|
13
|
+
|
|
14
|
+
s.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "verifip.gemspec"]
|
|
15
|
+
s.require_paths = ["lib"]
|
|
16
|
+
|
|
17
|
+
# Zero runtime dependencies — uses only Ruby stdlib (net/http, json, uri)
|
|
18
|
+
|
|
19
|
+
s.add_development_dependency "rspec", "~> 3.13"
|
|
20
|
+
s.add_development_dependency "webmock", "~> 3.23"
|
|
21
|
+
|
|
22
|
+
s.metadata = {
|
|
23
|
+
"bug_tracker_uri" => "https://github.com/verifip/verifip-ruby/issues",
|
|
24
|
+
"changelog_uri" => "https://github.com/verifip/verifip-ruby/blob/main/CHANGELOG.md",
|
|
25
|
+
"documentation_uri" => "https://docs.verifip.com",
|
|
26
|
+
"homepage_uri" => s.homepage,
|
|
27
|
+
"source_code_uri" => "https://github.com/verifip/verifip-ruby"
|
|
28
|
+
}
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: verifip
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- VerifIP
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.13'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.13'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: webmock
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.23'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.23'
|
|
40
|
+
description: Ruby client library for the VerifIP API. Check IP addresses for fraud
|
|
41
|
+
risk, VPN/proxy/Tor detection, geolocation, and more.
|
|
42
|
+
email:
|
|
43
|
+
- support@verifip.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- LICENSE
|
|
49
|
+
- README.md
|
|
50
|
+
- lib/verifip.rb
|
|
51
|
+
- lib/verifip/client.rb
|
|
52
|
+
- lib/verifip/errors.rb
|
|
53
|
+
- lib/verifip/models.rb
|
|
54
|
+
- lib/verifip/version.rb
|
|
55
|
+
- verifip.gemspec
|
|
56
|
+
homepage: https://github.com/verifip/verifip-ruby
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
bug_tracker_uri: https://github.com/verifip/verifip-ruby/issues
|
|
61
|
+
changelog_uri: https://github.com/verifip/verifip-ruby/blob/main/CHANGELOG.md
|
|
62
|
+
documentation_uri: https://docs.verifip.com
|
|
63
|
+
homepage_uri: https://github.com/verifip/verifip-ruby
|
|
64
|
+
source_code_uri: https://github.com/verifip/verifip-ruby
|
|
65
|
+
rdoc_options: []
|
|
66
|
+
require_paths:
|
|
67
|
+
- lib
|
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - ">="
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: 3.1.0
|
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: '0'
|
|
78
|
+
requirements: []
|
|
79
|
+
rubygems_version: 4.0.2
|
|
80
|
+
specification_version: 4
|
|
81
|
+
summary: Official Ruby SDK for the VerifIP IP fraud scoring API
|
|
82
|
+
test_files: []
|