internetdata 2.2.0 → 2.3.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.
@@ -0,0 +1,258 @@
1
+ =begin
2
+ #InternetData API
3
+
4
+ #The InternetData API: download the databases your organization is licensed for, and manage the account behind them. See https://docs.internetdata.io for guides and https://github.com/internetdata for the official client libraries.
5
+
6
+ The version of the OpenAPI document: 2026.09.21
7
+ Contact: support@internetdata.io
8
+ Generated by: https://openapi-generator.tech
9
+ Generator version: 7.25.0
10
+
11
+ =end
12
+
13
+ require 'date'
14
+ require 'time'
15
+
16
+ module InternetData
17
+ class TokenResponse < ApiModelBase
18
+ attr_accessor :access_token
19
+
20
+ # Always `Bearer`.
21
+ attr_accessor :token_type
22
+
23
+ # Seconds until the access token expires.
24
+ attr_accessor :expires_in
25
+
26
+ # Always returned. A refresh consumes the token it presents, so keep this one.
27
+ attr_accessor :refresh_token
28
+
29
+ # What was actually granted, which may be narrower than what was asked for.
30
+ attr_accessor :scope
31
+
32
+ # Not part of OAuth. The ID of the API key the person picked when they approved, returned by every grant while this authorization may still read that key back. Absent when no key was picked, or when the person's role no longer allows reading keys back.
33
+ attr_accessor :apikey_id
34
+
35
+ # Not part of OAuth. The API key itself, so a device ends up holding an ordinary key. Returned by the device code and authorization code grants only, never by a refresh, and only alongside `mslm:apikey_id`. Absent when that key's secret cannot be read back, which is the case for a key created before keys could be shown again in the console; a rotated key can be.
36
+ attr_accessor :apikey
37
+
38
+ # Attribute mapping from ruby-style variable name to JSON key.
39
+ def self.attribute_map
40
+ {
41
+ :'access_token' => :'access_token',
42
+ :'token_type' => :'token_type',
43
+ :'expires_in' => :'expires_in',
44
+ :'refresh_token' => :'refresh_token',
45
+ :'scope' => :'scope',
46
+ :'apikey_id' => :'mslm:apikey_id',
47
+ :'apikey' => :'mslm:apikey'
48
+ }
49
+ end
50
+
51
+ # Returns attribute mapping this model knows about
52
+ def self.acceptable_attribute_map
53
+ attribute_map
54
+ end
55
+
56
+ # Returns all the JSON keys this model knows about
57
+ def self.acceptable_attributes
58
+ acceptable_attribute_map.values
59
+ end
60
+
61
+ # Attribute type mapping.
62
+ def self.openapi_types
63
+ {
64
+ :'access_token' => :'String',
65
+ :'token_type' => :'String',
66
+ :'expires_in' => :'Integer',
67
+ :'refresh_token' => :'String',
68
+ :'scope' => :'String',
69
+ :'apikey_id' => :'String',
70
+ :'apikey' => :'String'
71
+ }
72
+ end
73
+
74
+ # List of attributes with nullable: true
75
+ def self.openapi_nullable
76
+ Set.new([
77
+ ])
78
+ end
79
+
80
+ # Initializes the object
81
+ # @param [Hash] attributes Model attributes in the form of hash
82
+ def initialize(attributes = {})
83
+ if (!attributes.is_a?(Hash))
84
+ fail ArgumentError, "The input argument (attributes) must be a hash in `InternetData::TokenResponse` initialize method"
85
+ end
86
+
87
+ # check to see if the attribute exists and convert string to symbol for hash key
88
+ acceptable_attribute_map = self.class.acceptable_attribute_map
89
+ attributes = attributes.each_with_object({}) { |(k, v), h|
90
+ if (!acceptable_attribute_map.key?(k.to_sym))
91
+ fail ArgumentError, "`#{k}` is not a valid attribute in `InternetData::TokenResponse`. Please check the name to make sure it's valid. List of attributes: " + acceptable_attribute_map.keys.inspect
92
+ end
93
+ h[k.to_sym] = v
94
+ }
95
+
96
+ if attributes.key?(:'access_token')
97
+ self.access_token = attributes[:'access_token']
98
+ else
99
+ self.access_token = nil
100
+ end
101
+
102
+ if attributes.key?(:'token_type')
103
+ self.token_type = attributes[:'token_type']
104
+ else
105
+ self.token_type = nil
106
+ end
107
+
108
+ if attributes.key?(:'expires_in')
109
+ self.expires_in = attributes[:'expires_in']
110
+ else
111
+ self.expires_in = nil
112
+ end
113
+
114
+ if attributes.key?(:'refresh_token')
115
+ self.refresh_token = attributes[:'refresh_token']
116
+ end
117
+
118
+ if attributes.key?(:'scope')
119
+ self.scope = attributes[:'scope']
120
+ end
121
+
122
+ if attributes.key?(:'apikey_id')
123
+ self.apikey_id = attributes[:'apikey_id']
124
+ end
125
+
126
+ if attributes.key?(:'apikey')
127
+ self.apikey = attributes[:'apikey']
128
+ end
129
+ end
130
+
131
+ # Show invalid properties with the reasons. Usually used together with valid?
132
+ # @return Array for valid properties with the reasons
133
+ def list_invalid_properties
134
+ warn '[DEPRECATED] the `list_invalid_properties` method is obsolete'
135
+ invalid_properties = Array.new
136
+ if @access_token.nil?
137
+ invalid_properties.push('invalid value for "access_token", access_token cannot be nil.')
138
+ end
139
+
140
+ if @token_type.nil?
141
+ invalid_properties.push('invalid value for "token_type", token_type cannot be nil.')
142
+ end
143
+
144
+ if @expires_in.nil?
145
+ invalid_properties.push('invalid value for "expires_in", expires_in cannot be nil.')
146
+ end
147
+
148
+ invalid_properties
149
+ end
150
+
151
+ # Check to see if the all the properties in the model are valid
152
+ # @return true if the model is valid
153
+ def valid?
154
+ warn '[DEPRECATED] the `valid?` method is obsolete'
155
+ return false if @access_token.nil?
156
+ return false if @token_type.nil?
157
+ return false if @expires_in.nil?
158
+ true
159
+ end
160
+
161
+ # Custom attribute writer method with validation
162
+ # @param [Object] access_token Value to be assigned
163
+ def access_token=(access_token)
164
+ if access_token.nil?
165
+ fail ArgumentError, 'access_token cannot be nil'
166
+ end
167
+
168
+ @access_token = access_token
169
+ end
170
+
171
+ # Custom attribute writer method with validation
172
+ # @param [Object] token_type Value to be assigned
173
+ def token_type=(token_type)
174
+ if token_type.nil?
175
+ fail ArgumentError, 'token_type cannot be nil'
176
+ end
177
+
178
+ @token_type = token_type
179
+ end
180
+
181
+ # Custom attribute writer method with validation
182
+ # @param [Object] expires_in Value to be assigned
183
+ def expires_in=(expires_in)
184
+ if expires_in.nil?
185
+ fail ArgumentError, 'expires_in cannot be nil'
186
+ end
187
+
188
+ @expires_in = expires_in
189
+ end
190
+
191
+ # Checks equality by comparing each attribute.
192
+ # @param [Object] Object to be compared
193
+ def ==(o)
194
+ return true if self.equal?(o)
195
+ self.class == o.class &&
196
+ access_token == o.access_token &&
197
+ token_type == o.token_type &&
198
+ expires_in == o.expires_in &&
199
+ refresh_token == o.refresh_token &&
200
+ scope == o.scope &&
201
+ apikey_id == o.apikey_id &&
202
+ apikey == o.apikey
203
+ end
204
+
205
+ # @see the `==` method
206
+ # @param [Object] Object to be compared
207
+ def eql?(o)
208
+ self == o
209
+ end
210
+
211
+ # Calculates hash code according to all attributes.
212
+ # @return [Integer] Hash code
213
+ def hash
214
+ [access_token, token_type, expires_in, refresh_token, scope, apikey_id, apikey].hash
215
+ end
216
+
217
+ # Builds the object from hash
218
+ # @param [Hash] attributes Model attributes in the form of hash
219
+ # @return [Object] Returns the model itself
220
+ def self.build_from_hash(attributes)
221
+ return nil unless attributes.is_a?(Hash)
222
+ attributes = attributes.transform_keys(&:to_sym)
223
+ transformed_hash = {}
224
+ openapi_types.each_pair do |key, type|
225
+ if attributes.key?(attribute_map[key]) && attributes[attribute_map[key]].nil?
226
+ transformed_hash["#{key}"] = nil
227
+ elsif type =~ /\AArray<(.*)>/i
228
+ # check to ensure the input is an array given that the attribute
229
+ # is documented as an array but the input is not
230
+ if attributes[attribute_map[key]].is_a?(Array)
231
+ transformed_hash["#{key}"] = attributes[attribute_map[key]].map { |v| _deserialize($1, v) }
232
+ end
233
+ elsif !attributes[attribute_map[key]].nil?
234
+ transformed_hash["#{key}"] = _deserialize(type, attributes[attribute_map[key]])
235
+ end
236
+ end
237
+ new(transformed_hash)
238
+ end
239
+
240
+ # Returns the object in the form of hash
241
+ # @return [Hash] Returns the object in the form of hash
242
+ def to_hash
243
+ hash = {}
244
+ self.class.attribute_map.each_pair do |attr, param|
245
+ value = self.send(attr)
246
+ if value.nil?
247
+ is_nullable = self.class.openapi_nullable.include?(attr)
248
+ next if !is_nullable || (is_nullable && !instance_variable_defined?(:"@#{attr}"))
249
+ end
250
+
251
+ hash[param] = _to_hash(value)
252
+ end
253
+ hash
254
+ end
255
+
256
+ end
257
+
258
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InternetData
4
+ # Signing a person in with OAuth, reached as `client.oauth`.
5
+ #
6
+ # A program on the person's own machine starts a device sign-in, shows them a
7
+ # code to approve in their browser, and waits for the tokens, which can carry
8
+ # one of their API keys so nobody pastes a key by hand. Only a registered
9
+ # client ID works; they are issued on request from support@internetdata.io.
10
+ #
11
+ # No request made here carries the API key this client was built with, and a
12
+ # client built without one works exactly the same. Every method takes
13
+ # `timeout:`, seconds per attempt, for that call alone.
14
+ class OauthApi
15
+ METADATA_PATH = '/.well-known/oauth-authorization-server'
16
+ DEVICE_AUTHORIZATION_PATH = '/oauth/device_authorization'
17
+ TOKEN_PATH = '/oauth/token'
18
+ REVOKE_PATH = '/oauth/revoke'
19
+ DEVICE_CODE_GRANT = 'urn:ietf:params:oauth:grant-type:device_code'
20
+
21
+ # The members a 2xx must carry for its type to mean anything.
22
+ REQUIRED = {
23
+ OauthMetadata => %i[issuer authorization_endpoint token_endpoint],
24
+ DeviceAuthorization => %i[device_code user_code verification_uri expires_in interval],
25
+ TokenResponse => %i[access_token token_type expires_in],
26
+ }.freeze
27
+
28
+ def initialize(transport, retries:)
29
+ @transport = transport
30
+ @retries = retries
31
+ # The poll's wait and its monotonic clock, which a test replaces together.
32
+ @wait = ->(seconds) { sleep(seconds) }
33
+ @now = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
34
+ end
35
+
36
+ # The authorization server's discovery document.
37
+ #
38
+ # @return [OauthMetadata]
39
+ def metadata(timeout: nil)
40
+ Retries.with_retries(@retries) do
41
+ decode(OauthMetadata, @transport.oauth_request(:GET, METADATA_PATH, timeout: timeout).run)
42
+ end
43
+ end
44
+
45
+ # Start a device sign-in: show the person `user_code` and `verification_uri`,
46
+ # then {#poll_device_token}.
47
+ #
48
+ # @param scope [String, nil] space-delimited, sent as given; the server grants
49
+ # what this client may ask for and silently drops the rest.
50
+ # @param resource [String, nil] the API the tokens are for.
51
+ # @return [DeviceAuthorization]
52
+ def device_authorization(client_id, scope: nil, resource: nil, timeout: nil)
53
+ form = { 'client_id' => client_id, 'scope' => scope, 'resource' => resource }.compact
54
+ Retries.with_retries(@retries) do
55
+ request = @transport.oauth_request(:POST, DEVICE_AUTHORIZATION_PATH, form: form, timeout: timeout)
56
+ decode(DeviceAuthorization, request.run)
57
+ end
58
+ end
59
+
60
+ # Exchange a device code for tokens, once. Until the person approves this
61
+ # raises {OauthRequestError} coded `authorization_pending`;
62
+ # {#poll_device_token} is the loop that waits for them.
63
+ #
64
+ # Never retried: the server spends the code when it answers, so a retry after
65
+ # a lost success could only fail and lose the tokens.
66
+ #
67
+ # @return [TokenResponse] with `apikey_id` and `apikey` when the person picked a key.
68
+ def exchange_device_code(client_id, device_code, timeout: nil)
69
+ form = { 'grant_type' => DEVICE_CODE_GRANT, 'device_code' => device_code, 'client_id' => client_id }
70
+ exchange(form, timeout)
71
+ end
72
+
73
+ # Exchange a refresh token for a new pair. The token presented is spent, so
74
+ # keep the `refresh_token` this returns. Never retried, for the same reason as
75
+ # {#exchange_device_code}.
76
+ #
77
+ # @return [TokenResponse] which may name the key in `apikey_id`, but never carries `apikey`.
78
+ def exchange_refresh_token(client_id, refresh_token, timeout: nil)
79
+ form = { 'grant_type' => 'refresh_token', 'refresh_token' => refresh_token, 'client_id' => client_id }
80
+ exchange(form, timeout)
81
+ end
82
+
83
+ # Revoke an access or refresh token. A refresh token ends the whole grant and
84
+ # every token it issued, which is how a machine signs out.
85
+ #
86
+ # @return [nil]
87
+ def revoke(client_id, token, timeout: nil)
88
+ form = { 'token' => token, 'client_id' => client_id }
89
+ Retries.with_retries(@retries) do
90
+ request = @transport.oauth_request(:POST, REVOKE_PATH, form: form, timeout: timeout)
91
+ Transport.oauth_success!(request.run)
92
+ end
93
+ nil
94
+ end
95
+
96
+ # Wait for the person to approve a device sign-in, and return its tokens.
97
+ #
98
+ # Waits `device.interval` seconds before EVERY exchange, the first included,
99
+ # and five seconds longer for good each time the server answers `slow_down`.
100
+ # Raises {OauthAccessDeniedError} when the person refuses and
101
+ # {OauthExpiredTokenError} when the code expires, including locally, with no
102
+ # status, once `device.expires_in` seconds have passed since this call. Any
103
+ # other failure, a timeout or an outage included, ends the wait unchanged;
104
+ # calling again with the same device is safe until the code expires.
105
+ #
106
+ # There is no way to cancel it from outside: it blocks until one of those
107
+ # outcomes, so run it where blocking is acceptable.
108
+ #
109
+ # @param timeout [Numeric, nil] bounds each exchange, never the whole wait.
110
+ # @return [TokenResponse]
111
+ def poll_device_token(client_id, device, timeout: nil)
112
+ interval = device.interval >= 1 ? device.interval : 5
113
+ deadline = @now.call + device.expires_in
114
+ loop do
115
+ @wait.call(interval)
116
+ raise OauthExpiredTokenError, 'expired_token' if @now.call >= deadline
117
+
118
+ begin
119
+ return exchange_device_code(client_id, device.device_code, timeout: timeout)
120
+ rescue OauthRequestError => e
121
+ case e.error_code
122
+ when 'slow_down' then interval += 5
123
+ when 'authorization_pending' then next
124
+ else raise
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ private
131
+
132
+ def exchange(form, timeout)
133
+ decode(TokenResponse, @transport.oauth_request(:POST, TOKEN_PATH, form: form, timeout: timeout).run)
134
+ end
135
+
136
+ # Only the members the type declares are read, each checked against the type
137
+ # the spec gives it, so an absent member stays nil and an empty `scope` stays
138
+ # an empty string. A 2xx that is not the type is the server's fault.
139
+ def decode(type, response)
140
+ body = Transport.oauth_object(response)
141
+ type.attribute_map.each do |member, wire|
142
+ value = body[wire.to_s]
143
+ if value.nil?
144
+ next unless REQUIRED.fetch(type).include?(member)
145
+
146
+ raise Error.new(:server_error, "the answer carried no #{wire}", status: response.code)
147
+ end
148
+ next if typed?(type.openapi_types.fetch(member), value)
149
+
150
+ raise Error.new(:server_error, "the answer's #{wire} is not a #{type.openapi_types[member]}",
151
+ status: response.code)
152
+ end
153
+ type.build_from_hash(body)
154
+ end
155
+
156
+ def typed?(type, value)
157
+ case type
158
+ when :String then value.is_a?(String)
159
+ when :Integer then value.is_a?(Integer)
160
+ when :Boolean then [true, false].include?(value)
161
+ when :'Array<String>' then value.is_a?(Array) && value.all?(String)
162
+ else raise ArgumentError, "no check for a member typed #{type}"
163
+ end
164
+ end
165
+ end
166
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
3
4
  require 'uri'
4
5
 
5
6
  module InternetData
@@ -87,5 +88,67 @@ module InternetData
87
88
  options[:cainfo] = @config.ssl_ca_cert if @config.ssl_ca_cert
88
89
  Typhoeus::Request.new(url, options)
89
90
  end
91
+
92
+ # A request to the authorization server, which carries NO credential whatever
93
+ # this client was built with: these endpoints have no use for the API key, and
94
+ # on the token endpoint an `Authorization` header reads as client
95
+ # authentication, which a public client does not have.
96
+ #
97
+ # The form is encoded here rather than by curl, so a `+` in a value leaves as
98
+ # `%2B` and never arrives as a space.
99
+ def oauth_request(http_method, path, form: nil, timeout: nil)
100
+ headers = { 'Accept' => 'application/json' }
101
+ headers['Content-Type'] = 'application/x-www-form-urlencoded' unless form.nil?
102
+ request = build_request(http_method, path, header_params: headers, auth_names: [], timeout: timeout)
103
+ request.options[:body] = URI.encode_www_form(form) unless form.nil?
104
+ request
105
+ end
106
+
107
+ # The JSON object a 2xx OAuth answer carries. A refusal the authorization
108
+ # server words as an RFC 6749 error raises that; anything else that is not a
109
+ # 2xx raises the ordinary error its status maps to.
110
+ def self.oauth_object(response)
111
+ oauth_success!(response)
112
+ parse_object(response)
113
+ end
114
+
115
+ def self.oauth_success!(response)
116
+ raise Error.from_transport(response) if transport_failure?(response)
117
+ return if response.success?
118
+
119
+ raise oauth_refusal(response) || Error.from_status(response.code, response.headers, response.body)
120
+ end
121
+
122
+ # Only a 4xx whose body is a JSON object with a STRING `error` is the
123
+ # authorization server's own refusal. A 5xx is an outage whatever its body
124
+ # says, and a gateway's page names no OAuth code at all.
125
+ def self.oauth_refusal(response)
126
+ return nil unless (400..499).cover?(response.code)
127
+
128
+ body = JSON.parse(response.body.to_s)
129
+ return nil unless body.is_a?(Hash) && body['error'].is_a?(String)
130
+
131
+ description = body['error_description'].is_a?(String) ? body['error_description'] : nil
132
+ OauthRequestError.for_code(body['error'], description, status: response.code, headers: response.headers)
133
+ rescue JSON::ParserError
134
+ nil
135
+ end
136
+
137
+ def self.transport_failure?(response)
138
+ response.timed_out? || response.code.to_i.zero?
139
+ end
140
+
141
+ def self.parse_object(response)
142
+ body = JSON.parse(response.body.to_s)
143
+ return body if body.is_a?(Hash)
144
+
145
+ raise Error.new(:server_error, 'the API answered with something other than an object',
146
+ status: response.code)
147
+ rescue JSON::ParserError => e
148
+ raise Error.new(:server_error, "could not parse the response body: #{e.message}",
149
+ status: response.code)
150
+ end
151
+
152
+ private_class_method :transport_failure?, :parse_object, :oauth_refusal
90
153
  end
91
154
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module InternetData
4
- VERSION = '2.2.0'
4
+ VERSION = '2.3.0'
5
5
  end
data/lib/internetdata.rb CHANGED
@@ -16,6 +16,7 @@ require 'internetdata/errors'
16
16
  require 'internetdata/retries'
17
17
  require 'internetdata/transport'
18
18
  require 'internetdata/database_api'
19
+ require 'internetdata/oauth_api'
19
20
  require 'internetdata/client'
20
21
 
21
22
  # The official Ruby client library for the InternetData API.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: internetdata
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mslm Dev
@@ -50,10 +50,14 @@ files:
50
50
  - lib/internetdata/models/database_metadata_column.rb
51
51
  - lib/internetdata/models/database_version.rb
52
52
  - lib/internetdata/models/db_checksums.rb
53
+ - lib/internetdata/models/device_authorization.rb
53
54
  - lib/internetdata/models/download.rb
54
55
  - lib/internetdata/models/download_list.rb
55
56
  - lib/internetdata/models/error_envelope.rb
57
+ - lib/internetdata/models/oauth_metadata.rb
56
58
  - lib/internetdata/models/standing.rb
59
+ - lib/internetdata/models/token_response.rb
60
+ - lib/internetdata/oauth_api.rb
57
61
  - lib/internetdata/retries.rb
58
62
  - lib/internetdata/transport.rb
59
63
  - lib/internetdata/version.rb