masklen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6079eabf7927a845e4bf6c10badae83dac6e028f2a0e4c7815a8d2745723c295
4
+ data.tar.gz: 6ee8e4a6f9c0084b979729060d5388d4044bef02c01ace8c2ccab58438506c2f
5
+ SHA512:
6
+ metadata.gz: 1164448a63590a6869f555a5c0e360938bd4562124eb3d5aacae37c11fbf072bb53e164c0b0eb30de998582802c127ad4164d0b50421d193bac293f4024a4383
7
+ data.tar.gz: 75f709d8290831b5b68809158c610d4197ceeea85ecc573417d2a943c4296098f12803686f67ecee6dc4036bbbd516d10c07c8384e8c7562e2b94e2ec0fe13ab
data/README.md ADDED
@@ -0,0 +1,183 @@
1
+ # masklen
2
+
3
+ Official Ruby SDK for the [masklen.dev](https://masklen.dev) IP intelligence API.
4
+
5
+ Look up geolocation, network, privacy, and locale data for any IPv4 or IPv6 address. Zero external dependencies -- uses only Ruby stdlib.
6
+
7
+ ## Installation
8
+
9
+ Add to your Gemfile:
10
+
11
+ ```ruby
12
+ gem "masklen"
13
+ ```
14
+
15
+ Or install directly:
16
+
17
+ ```sh
18
+ gem install masklen
19
+ ```
20
+
21
+ ## Quick start
22
+
23
+ ```ruby
24
+ require "masklen"
25
+
26
+ client = Masklen::Client.new(api_key: "your-api-key")
27
+
28
+ # 1. Look up a specific IP address
29
+ result = client.lookup("8.8.8.8")
30
+ puts result.ip # "8.8.8.8"
31
+ puts result.location.country # "United States"
32
+ puts result.network.isp # "Google LLC"
33
+ puts result.privacy.threat_level # "low"
34
+
35
+ # 2. Look up your own IP (the caller's IP as seen by the server)
36
+ self_result = client.lookup_self
37
+ puts self_result.location.city
38
+
39
+ # 3. Batch lookup (up to 1000 IPs per request)
40
+ batch = client.lookup_batch(["8.8.8.8", "1.1.1.1"])
41
+ batch.results.each do |r|
42
+ if r.is_a?(Masklen::LookupResult)
43
+ puts "#{r.ip} -> #{r.location&.country}"
44
+ else
45
+ # r is a Masklen::BatchItemError
46
+ puts "#{r.ip} error: #{r.error_message}"
47
+ end
48
+ end
49
+ ```
50
+
51
+ ## Method signatures
52
+
53
+ ```ruby
54
+ # Initialise the client
55
+ client = Masklen::Client.new(
56
+ api_key: "your-api-key", # required
57
+ base_url: "https://masklen.dev" # optional, defaults to production
58
+ )
59
+
60
+ # Single IP lookup
61
+ result = client.lookup(ip, fields: nil)
62
+
63
+ # Caller's own IP
64
+ result = client.lookup_self(fields: nil)
65
+
66
+ # Batch lookup
67
+ batch = client.lookup_batch(ips, fields: nil)
68
+ ```
69
+
70
+ ## Filtering fields
71
+
72
+ All three methods accept an optional `fields:` array. Pass one or more of:
73
+
74
+ - `"location"` -- city, region, country, coordinates, timezone
75
+ - `"network"` -- ASN, ISP, organisation, domain
76
+ - `"privacy"` -- VPN, proxy, Tor, hosting, threat level
77
+ - `"locale"` -- currency, calling code, languages, flag
78
+
79
+ ```ruby
80
+ # Fetch only location and privacy data to reduce response size
81
+ result = client.lookup("8.8.8.8", fields: ["location", "privacy"])
82
+ puts result.location.city
83
+ puts result.privacy.vpn
84
+
85
+ # Omit fields entirely to receive all data
86
+ result = client.lookup("8.8.8.8")
87
+ ```
88
+
89
+ ## Response types
90
+
91
+ All types are `Struct` subclasses with keyword arguments.
92
+
93
+ ### LookupResult
94
+
95
+ | Field | Type |
96
+ |------------|--------------|
97
+ | `ip` | String |
98
+ | `location` | Location, nil |
99
+ | `network` | Network, nil |
100
+ | `privacy` | Privacy, nil |
101
+ | `locale` | Locale, nil |
102
+
103
+ ### Location
104
+
105
+ | Field | Type |
106
+ |----------------|--------------|
107
+ | `city` | String, nil |
108
+ | `region` | String, nil |
109
+ | `country` | String, nil |
110
+ | `country_code` | String, nil |
111
+ | `latitude` | Float, nil |
112
+ | `longitude` | Float, nil |
113
+ | `postal_code` | String, nil |
114
+ | `timezone` | String, nil |
115
+
116
+ ### Network
117
+
118
+ | Field | Type |
119
+ |----------------|-------------|
120
+ | `asn` | String, nil |
121
+ | `isp` | String, nil |
122
+ | `organization` | String, nil |
123
+ | `domain` | String, nil |
124
+
125
+ ### Privacy
126
+
127
+ | Field | Type |
128
+ |----------------|---------|
129
+ | `vpn` | Boolean |
130
+ | `proxy` | Boolean |
131
+ | `tor` | Boolean |
132
+ | `hosting` | Boolean |
133
+ | `threat_level` | String ("low" or "medium") |
134
+
135
+ ### Locale
136
+
137
+ | Field | Type |
138
+ |-------------------|---------------|
139
+ | `currency` | String, nil |
140
+ | `currency_symbol` | String, nil |
141
+ | `calling_code` | String, nil |
142
+ | `languages` | Array<String> |
143
+ | `flag` | String, nil |
144
+
145
+ ### BatchResult
146
+
147
+ | Field | Type |
148
+ |-----------|---------------------------------------|
149
+ | `results` | Array<LookupResult, BatchItemError> |
150
+
151
+ ### BatchItemError
152
+
153
+ | Field | Type |
154
+ |-----------------|--------|
155
+ | `ip` | String |
156
+ | `error_code` | String |
157
+ | `error_message` | String |
158
+
159
+ ## Error handling
160
+
161
+ All API errors raise `Masklen::Error`, a subclass of `StandardError`.
162
+
163
+ ```ruby
164
+ begin
165
+ result = client.lookup("8.8.8.8")
166
+ rescue Masklen::Error => e
167
+ puts e.status_code # HTTP status, e.g. 401
168
+ puts e.error_code # machine-readable code, e.g. "unauthorized"
169
+ puts e.error_message # human-readable message
170
+ puts e.message # full formatted message string
171
+ end
172
+ ```
173
+
174
+ Network errors (timeouts, connection refused, DNS failures) are also wrapped in `Masklen::Error` with `status_code: 0` and `error_code: "network_error"`.
175
+
176
+ ## Requirements
177
+
178
+ - Ruby 3.0 or later
179
+ - No external runtime dependencies (stdlib only: `net/http`, `uri`, `json`)
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,170 @@
1
+ require "net/http"
2
+ require "uri"
3
+ require "json"
4
+
5
+ module Masklen
6
+ # Client is the main entry point for the masklen.dev IP intelligence API.
7
+ #
8
+ # Usage:
9
+ # client = Masklen::Client.new(api_key: "your-key")
10
+ # result = client.lookup("8.8.8.8")
11
+ # puts result.location.country
12
+ class Client
13
+ DEFAULT_BASE_URL = "https://masklen.dev"
14
+
15
+ # Create a new client.
16
+ #
17
+ # api_key - Your masklen.dev API key (required).
18
+ # base_url - Override the default base URL (optional, useful for testing).
19
+ def initialize(api_key:, base_url: DEFAULT_BASE_URL)
20
+ raise ArgumentError, "api_key is required" if api_key.nil? || api_key.strip.empty?
21
+
22
+ @api_key = api_key
23
+ @base_url = base_url.chomp("/")
24
+ end
25
+
26
+ # Look up a specific IPv4 or IPv6 address.
27
+ #
28
+ # ip - The IP address string to look up.
29
+ # fields - Optional array of field groups to include in the response,
30
+ # e.g. ["location", "privacy"]. When nil, the API returns all fields.
31
+ #
32
+ # Returns a LookupResult.
33
+ # Raises Masklen::Error on API errors.
34
+ def lookup(ip, fields: nil)
35
+ raise ArgumentError, "ip is required" if ip.nil? || ip.to_s.strip.empty?
36
+
37
+ query = build_fields_query(fields)
38
+ hash = request(:get, "/v1/lookup/#{URI.encode_www_form_component(ip.to_s)}", query: query)
39
+ LookupResult.from_hash(hash)
40
+ end
41
+
42
+ # Look up the caller's own IP address (as seen by the API server).
43
+ #
44
+ # fields - Optional array of field groups to include in the response.
45
+ #
46
+ # Returns a LookupResult.
47
+ # Raises Masklen::Error on API errors.
48
+ def lookup_self(fields: nil)
49
+ query = build_fields_query(fields)
50
+ hash = request(:get, "/v1/lookup", query: query)
51
+ LookupResult.from_hash(hash)
52
+ end
53
+
54
+ # Look up up to 1000 IP addresses in a single request.
55
+ #
56
+ # ips - Array of IP address strings (max 1000).
57
+ # fields - Optional array of field groups to include in the response.
58
+ #
59
+ # Returns a BatchResult whose results array contains LookupResult or
60
+ # BatchItemError objects.
61
+ # Raises Masklen::Error on API errors.
62
+ def lookup_batch(ips, fields: nil)
63
+ raise ArgumentError, "ips must be an Array" unless ips.is_a?(Array)
64
+ raise ArgumentError, "ips cannot be empty" if ips.empty?
65
+ raise ArgumentError, "ips cannot exceed 1000 entries" if ips.length > 1000
66
+
67
+ query = build_fields_query(fields)
68
+ body = JSON.generate({ "ips" => ips })
69
+ hash = request(:post, "/v1/lookup/batch", query: query, body: body)
70
+ BatchResult.from_hash(hash)
71
+ end
72
+
73
+ private
74
+
75
+ # Build the query hash for the optional fields parameter.
76
+ # Returns nil when fields is nil or empty.
77
+ def build_fields_query(fields)
78
+ return nil if fields.nil? || fields.empty?
79
+
80
+ { "fields" => fields.join(",") }
81
+ end
82
+
83
+ # Perform an HTTP request against the masklen.dev API.
84
+ #
85
+ # method - :get or :post (Symbol).
86
+ # path - URL path string, e.g. "/v1/lookup/8.8.8.8".
87
+ # query - Optional Hash of query-string parameters.
88
+ # body - Optional request body string (used for POST).
89
+ #
90
+ # Returns a parsed Hash from the JSON response body.
91
+ # Raises Masklen::Error when the server responds with a non-2xx status.
92
+ # Raises Masklen::Error wrapping network failures (SocketError, Timeout::Error, etc.).
93
+ def request(method, path, query: nil, body: nil)
94
+ uri = build_uri(path, query)
95
+
96
+ http = Net::HTTP.new(uri.host, uri.port)
97
+ http.use_ssl = (uri.scheme == "https")
98
+ http.read_timeout = 30
99
+ http.open_timeout = 10
100
+
101
+ req = build_request(method, uri, body)
102
+
103
+ begin
104
+ response = http.start { |conn| conn.request(req) }
105
+ rescue SocketError, Errno::ECONNREFUSED, Errno::ETIMEDOUT,
106
+ Timeout::Error, Net::OpenTimeout, Net::ReadTimeout => e
107
+ raise Masklen::Error.new(
108
+ status_code: 0,
109
+ error_code: "network_error",
110
+ error_message: e.message
111
+ )
112
+ end
113
+
114
+ parse_response(response)
115
+ end
116
+
117
+ # Construct the full URI for a request, appending query parameters when present.
118
+ def build_uri(path, query)
119
+ uri = URI.parse("#{@base_url}#{path}")
120
+ if query && !query.empty?
121
+ uri.query = URI.encode_www_form(query)
122
+ end
123
+ uri
124
+ end
125
+
126
+ # Build a Net::HTTP request object with the required headers.
127
+ def build_request(method, uri, body)
128
+ req = case method
129
+ when :get then Net::HTTP::Get.new(uri)
130
+ when :post then Net::HTTP::Post.new(uri)
131
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
132
+ end
133
+
134
+ req["Authorization"] = "Bearer #{@api_key}"
135
+ req["Accept"] = "application/json"
136
+ req["User-Agent"] = "masklen-ruby/#{Masklen::VERSION}"
137
+
138
+ if body
139
+ req["Content-Type"] = "application/json"
140
+ req.body = body
141
+ end
142
+
143
+ req
144
+ end
145
+
146
+ # Parse the HTTP response into a Hash.
147
+ # Raises Masklen::Error for non-2xx status codes.
148
+ def parse_response(response)
149
+ status = response.code.to_i
150
+
151
+ parsed = begin
152
+ JSON.parse(response.body)
153
+ rescue JSON::ParserError
154
+ {}
155
+ end
156
+
157
+ return parsed if status >= 200 && status < 300
158
+
159
+ # Attempt to extract API error details from the response body.
160
+ error_code = parsed["error_code"] || "unknown_error"
161
+ error_message = parsed["error_message"] || parsed["message"] || response.message || "Unknown error"
162
+
163
+ raise Masklen::Error.new(
164
+ status_code: status,
165
+ error_code: error_code,
166
+ error_message: error_message
167
+ )
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,18 @@
1
+ module Masklen
2
+ # Raised when the API returns a non-2xx response or when a network error occurs.
3
+ #
4
+ # Attributes:
5
+ # status_code - HTTP status code returned by the server (Integer)
6
+ # error_code - machine-readable error code from the API response body (String)
7
+ # error_message - human-readable description from the API response body (String)
8
+ class Error < StandardError
9
+ attr_reader :status_code, :error_code, :error_message
10
+
11
+ def initialize(status_code:, error_code:, error_message:)
12
+ @status_code = status_code
13
+ @error_code = error_code
14
+ @error_message = error_message
15
+ super("Masklen API error #{status_code} (#{error_code}): #{error_message}")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,158 @@
1
+ module Masklen
2
+ # Geographic location data for an IP address.
3
+ Location = Struct.new(
4
+ :city,
5
+ :region,
6
+ :country,
7
+ :country_code,
8
+ :latitude,
9
+ :longitude,
10
+ :postal_code,
11
+ :timezone,
12
+ keyword_init: true
13
+ ) do
14
+ # Build a Location from a parsed JSON hash. Returns nil if hash is nil.
15
+ def self.from_hash(hash)
16
+ return nil if hash.nil?
17
+
18
+ new(
19
+ city: hash["city"],
20
+ region: hash["region"],
21
+ country: hash["country"],
22
+ country_code: hash["country_code"],
23
+ latitude: hash["latitude"],
24
+ longitude: hash["longitude"],
25
+ postal_code: hash["postal_code"],
26
+ timezone: hash["timezone"]
27
+ )
28
+ end
29
+ end
30
+
31
+ # Network and ASN data for an IP address.
32
+ Network = Struct.new(
33
+ :asn,
34
+ :isp,
35
+ :organization,
36
+ :domain,
37
+ keyword_init: true
38
+ ) do
39
+ # Build a Network from a parsed JSON hash. Returns nil if hash is nil.
40
+ def self.from_hash(hash)
41
+ return nil if hash.nil?
42
+
43
+ new(
44
+ asn: hash["asn"],
45
+ isp: hash["isp"],
46
+ organization: hash["organization"],
47
+ domain: hash["domain"]
48
+ )
49
+ end
50
+ end
51
+
52
+ # Privacy and threat indicators for an IP address.
53
+ #
54
+ # threat_level is "low" or "medium".
55
+ Privacy = Struct.new(
56
+ :vpn,
57
+ :proxy,
58
+ :tor,
59
+ :hosting,
60
+ :threat_level,
61
+ keyword_init: true
62
+ ) do
63
+ # Build a Privacy from a parsed JSON hash. Returns nil if hash is nil.
64
+ def self.from_hash(hash)
65
+ return nil if hash.nil?
66
+
67
+ new(
68
+ vpn: hash["vpn"],
69
+ proxy: hash["proxy"],
70
+ tor: hash["tor"],
71
+ hosting: hash["hosting"],
72
+ threat_level: hash["threat_level"]
73
+ )
74
+ end
75
+ end
76
+
77
+ # Locale and regional data for an IP address.
78
+ Locale = Struct.new(
79
+ :currency,
80
+ :currency_symbol,
81
+ :calling_code,
82
+ :languages,
83
+ :flag,
84
+ keyword_init: true
85
+ ) do
86
+ # Build a Locale from a parsed JSON hash. Returns nil if hash is nil.
87
+ def self.from_hash(hash)
88
+ return nil if hash.nil?
89
+
90
+ new(
91
+ currency: hash["currency"],
92
+ currency_symbol: hash["currency_symbol"],
93
+ calling_code: hash["calling_code"],
94
+ languages: hash["languages"] || [],
95
+ flag: hash["flag"]
96
+ )
97
+ end
98
+ end
99
+
100
+ # The primary result returned for a single IP lookup.
101
+ LookupResult = Struct.new(
102
+ :ip,
103
+ :location,
104
+ :network,
105
+ :privacy,
106
+ :locale,
107
+ keyword_init: true
108
+ ) do
109
+ # Build a LookupResult from a parsed JSON hash.
110
+ def self.from_hash(hash)
111
+ new(
112
+ ip: hash["ip"],
113
+ location: Location.from_hash(hash["location"]),
114
+ network: Network.from_hash(hash["network"]),
115
+ privacy: Privacy.from_hash(hash["privacy"]),
116
+ locale: Locale.from_hash(hash["locale"])
117
+ )
118
+ end
119
+ end
120
+
121
+ # Represents a failed entry inside a batch lookup response.
122
+ BatchItemError = Struct.new(
123
+ :ip,
124
+ :error_code,
125
+ :error_message,
126
+ keyword_init: true
127
+ ) do
128
+ # Build a BatchItemError from a parsed JSON hash.
129
+ def self.from_hash(hash)
130
+ new(
131
+ ip: hash["ip"],
132
+ error_code: hash["error_code"],
133
+ error_message: hash["error_message"]
134
+ )
135
+ end
136
+ end
137
+
138
+ # The result of a batch IP lookup. Each entry in results is either a
139
+ # LookupResult or a BatchItemError.
140
+ BatchResult = Struct.new(
141
+ :results,
142
+ keyword_init: true
143
+ ) do
144
+ # Build a BatchResult from a parsed JSON hash.
145
+ # Each element in the "results" array is mapped to a LookupResult when it
146
+ # has an "ip" key and no "error_code", or to a BatchItemError otherwise.
147
+ def self.from_hash(hash)
148
+ items = (hash["results"] || []).map do |item|
149
+ if item.key?("error_code")
150
+ BatchItemError.from_hash(item)
151
+ else
152
+ LookupResult.from_hash(item)
153
+ end
154
+ end
155
+ new(results: items)
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,3 @@
1
+ module Masklen
2
+ VERSION = "1.0.0"
3
+ end
data/lib/masklen.rb ADDED
@@ -0,0 +1,25 @@
1
+ require_relative "masklen/version"
2
+ require_relative "masklen/error"
3
+ require_relative "masklen/types"
4
+ require_relative "masklen/client"
5
+
6
+ # Masklen is the official Ruby SDK for the masklen.dev IP intelligence API.
7
+ #
8
+ # Quick start:
9
+ # require "masklen"
10
+ #
11
+ # client = Masklen::Client.new(api_key: "your-api-key")
12
+ #
13
+ # # Look up a specific IP
14
+ # result = client.lookup("8.8.8.8")
15
+ # puts result.location.country
16
+ #
17
+ # # Look up the caller's own IP
18
+ # self_result = client.lookup_self(fields: ["location", "privacy"])
19
+ # puts self_result.privacy.vpn
20
+ #
21
+ # # Batch lookup
22
+ # batch = client.lookup_batch(["8.8.8.8", "1.1.1.1"])
23
+ # batch.results.each { |r| puts r.ip }
24
+ module Masklen
25
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: masklen
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - masklen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-24 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Look up geolocation, network, privacy, and locale data for any IP address.
14
+ email:
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/masklen.rb
21
+ - lib/masklen/client.rb
22
+ - lib/masklen/error.rb
23
+ - lib/masklen/types.rb
24
+ - lib/masklen/version.rb
25
+ homepage: https://masklen.dev
26
+ licenses:
27
+ - MIT
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: 3.0.0
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 3.0.3.1
45
+ signing_key:
46
+ specification_version: 4
47
+ summary: Official Ruby SDK for the masklen.dev IP intelligence API
48
+ test_files: []