onetime 0.6.0 → 0.7.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.
@@ -1,224 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
-
7
- require_relative "version"
8
- require_relative "response"
9
- require_relative "errors"
10
-
11
- module Onetime
12
- # Zero-dependency HTTP transport built on stdlib Net::HTTP.
13
- #
14
- # Responsibilities:
15
- # - build the request URL, headers and body (form for v1, JSON for v2)
16
- # - apply HTTP Basic auth when credentials are configured
17
- # - retry idempotent requests with exponential backoff
18
- # - parse the JSON response and map error statuses to exceptions
19
- #
20
- # It intentionally has no knowledge of API versions or resources; callers
21
- # pass fully-qualified paths (e.g. "/api/v2/secret/conceal").
22
- class Transport
23
- IDEMPOTENT_METHODS = %i[get head].freeze
24
- RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
25
- RETRYABLE_ERRORS = [
26
- Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
27
- Errno::ETIMEDOUT, EOFError, SocketError, IOError,
28
- Net::OpenTimeout, Net::ReadTimeout
29
- ].freeze
30
-
31
- METHOD_CLASSES = {
32
- get: Net::HTTP::Get,
33
- post: Net::HTTP::Post,
34
- patch: Net::HTTP::Patch,
35
- put: Net::HTTP::Put,
36
- delete: Net::HTTP::Delete,
37
- head: Net::HTTP::Head,
38
- }.freeze
39
-
40
- def initialize(config)
41
- @config = config
42
- end
43
-
44
- # Execute an HTTP request.
45
- #
46
- # @param method [Symbol] :get, :post, :patch, ...
47
- # @param path [String] fully-qualified path including the /api/vN prefix
48
- # @param query [Hash, nil] query-string parameters
49
- # @param body [Hash, nil] request body serialized as JSON
50
- # @param form [Hash, nil] request body serialized as form-urlencoded
51
- # @param headers [Hash] extra request headers
52
- # @param raise_on_error [Boolean] raise APIError for status >= 400
53
- # @return [Onetime::Response]
54
- def request(method, path, query: nil, body: nil, form: nil, headers: {}, raise_on_error: true)
55
- uri = build_uri(path, query)
56
- attempt = 0
57
-
58
- begin
59
- response = perform(method, uri, body: body, form: form, headers: headers)
60
-
61
- if response.http_status >= 400
62
- if retryable_status?(response.http_status) && retry_allowed?(method, attempt)
63
- attempt += 1
64
- backoff(attempt)
65
- raise Retry
66
- end
67
- raise Errors.from_response(response) if raise_on_error
68
- end
69
-
70
- response
71
- rescue Retry
72
- retry
73
- rescue *RETRYABLE_ERRORS => e
74
- if retry_allowed?(method, attempt)
75
- attempt += 1
76
- backoff(attempt)
77
- retry
78
- end
79
- raise wrap_transport_error(e)
80
- end
81
- end
82
-
83
- # Encodes a Hash as application/x-www-form-urlencoded, expanding Array
84
- # values into repeated `key[]=` pairs (Rack's array convention, which
85
- # the v1 API relies on for `recipient`).
86
- def self.encode_form(hash)
87
- pairs = []
88
- hash.each do |key, value|
89
- next if value.nil?
90
-
91
- if value.is_a?(Array)
92
- value.each { |v| pairs << ["#{key}[]", v.to_s] }
93
- else
94
- pairs << [key.to_s, value.to_s]
95
- end
96
- end
97
- URI.encode_www_form(pairs)
98
- end
99
-
100
- private
101
-
102
- # Sentinel used to trigger a retry from within the begin/rescue.
103
- Retry = Class.new(StandardError)
104
- private_constant :Retry
105
-
106
- def perform(method, uri, body:, form:, headers:)
107
- request = build_request(method, uri, body: body, form: form, headers: headers)
108
- log(:debug) { "#{method.to_s.upcase} #{uri}" }
109
-
110
- http = Net::HTTP.new(uri.host, uri.port)
111
- http.use_ssl = uri.scheme == "https"
112
- http.open_timeout = @config.open_timeout
113
- http.read_timeout = @config.timeout
114
-
115
- raw = http.request(request)
116
- build_response(raw)
117
- end
118
-
119
- def build_request(method, uri, body:, form:, headers:)
120
- klass = METHOD_CLASSES.fetch(method) do
121
- raise ArgumentError, "Unsupported HTTP method: #{method.inspect}"
122
- end
123
- request = klass.new(uri)
124
-
125
- default_headers.merge(headers).each { |k, v| request[k] = v }
126
-
127
- unless @config.anonymous?
128
- # HTTP Basic: the customer extid occupies the username slot,
129
- # the API token occupies the password slot.
130
- request.basic_auth(@config.customer, @config.api_token)
131
- end
132
-
133
- if form
134
- request["Content-Type"] = "application/x-www-form-urlencoded"
135
- request.body = self.class.encode_form(form)
136
- elsif body
137
- request["Content-Type"] = "application/json"
138
- request.body = JSON.generate(body)
139
- end
140
-
141
- request
142
- end
143
-
144
- def build_response(raw)
145
- status = raw.code.to_i
146
- raw_body = raw.body.to_s
147
- data = parse_body(raw, raw_body)
148
-
149
- Response.new(
150
- http_status: status,
151
- headers: raw.to_hash,
152
- raw_body: raw_body,
153
- data: data,
154
- )
155
- end
156
-
157
- def parse_body(raw, raw_body)
158
- return nil if raw_body.empty?
159
-
160
- content_type = raw["content-type"].to_s
161
- return raw_body unless content_type.include?("json")
162
-
163
- JSON.parse(raw_body)
164
- rescue JSON::ParserError
165
- # A non-JSON body on an otherwise-JSON endpoint: surface it raw rather
166
- # than blowing up, so callers can still inspect it.
167
- raw_body
168
- end
169
-
170
- def build_uri(path, query)
171
- uri = URI.join(ensure_trailing_slash(@config.base_url), path.sub(%r{\A/}, ""))
172
- if query && !query.empty?
173
- uri.query = self.class.encode_form(query)
174
- end
175
- uri
176
- end
177
-
178
- # URI.join treats the base as a directory only when it ends in "/".
179
- def ensure_trailing_slash(url)
180
- url.end_with?("/") ? url : "#{url}/"
181
- end
182
-
183
- def default_headers
184
- {
185
- "Accept" => "application/json",
186
- "User-Agent" => @config.user_agent || default_user_agent,
187
- "X-Onetime-Client" => "ruby:#{RUBY_VERSION}/#{Onetime::VERSION}",
188
- }.merge(@config.default_headers)
189
- end
190
-
191
- # Identifies the SDK, not the gem: "onetime-ruby" tells the service which
192
- # of the per-language clients is calling. The gem itself is `onetime`.
193
- def default_user_agent
194
- "onetime-ruby/#{Onetime::VERSION} (Ruby/#{RUBY_VERSION})"
195
- end
196
-
197
- def retry_allowed?(method, attempt)
198
- IDEMPOTENT_METHODS.include?(method) && attempt < @config.max_retries
199
- end
200
-
201
- def retryable_status?(status)
202
- RETRYABLE_STATUSES.include?(status)
203
- end
204
-
205
- # Exponential backoff: 0.5s, 1s, 2s, ...
206
- def backoff(attempt)
207
- sleep(0.5 * (2**(attempt - 1)))
208
- end
209
-
210
- def wrap_transport_error(error)
211
- if error.is_a?(Net::OpenTimeout) || error.is_a?(Net::ReadTimeout)
212
- TimeoutError.new("Request timed out: #{error.message}")
213
- else
214
- TransportError.new("Transport failure: #{error.message}")
215
- end
216
- end
217
-
218
- def log(level)
219
- return unless @config.logger
220
-
221
- @config.logger.public_send(level, "[onetime] #{yield}")
222
- end
223
- end
224
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Onetime
4
- # Library version. Source of truth for the gemspec, the release workflow's
5
- # tag check, and the X-Onetime-Client / User-Agent request headers.
6
- #
7
- # Note for anyone comparing against RubyGems: 0.5.1 (2013) and earlier are
8
- # the command-line tool that shipped under this gem name. 0.6.0 is the
9
- # cleaned-up client library.
10
- VERSION = "0.6.0"
11
- end
data/lib/onetime.rb DELETED
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "onetime/version"
4
- require_relative "onetime/errors"
5
- require_relative "onetime/configuration"
6
- require_relative "onetime/response"
7
- require_relative "onetime/transport"
8
- require_relative "onetime/client"
9
-
10
- # Onetime is the official Ruby client for the OnetimeSecret API.
11
- #
12
- # It supports the v1 and v2 APIs over a zero-dependency, stdlib-only
13
- # transport. See Onetime::Client for the primary interface.
14
- #
15
- # require "onetime"
16
- #
17
- # client = Onetime::Client.new(
18
- # base_url: "https://ca.onetimesecret.com",
19
- # customer: "ur1abc23def",
20
- # api_token: ENV["ONETIME_API_TOKEN"],
21
- # api_version: :v2,
22
- # )
23
- # res = client.secrets.conceal(secret: "hunter2", ttl: 3600)
24
- # res.dig("record", "secret", "secret_value")
25
- module Onetime
26
- # Convenience constructor mirroring Onetime::Client.new.
27
- #
28
- # Onetime.client(base_url: "https://ca.onetimesecret.com",
29
- # customer: "ur1abc23def", api_token: "...")
30
- def self.client(**options)
31
- Client.new(**options)
32
- end
33
- end