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.
- checksums.yaml +4 -4
- data/README.md +10 -240
- metadata +19 -18
- data/CHANGES.txt +0 -102
- data/lib/onetime/client.rb +0 -100
- data/lib/onetime/configuration.rb +0 -156
- data/lib/onetime/errors.rb +0 -160
- data/lib/onetime/resources/receipts.rb +0 -70
- data/lib/onetime/resources/secrets.rb +0 -134
- data/lib/onetime/response.rb +0 -72
- data/lib/onetime/transport.rb +0 -224
- data/lib/onetime/version.rb +0 -11
- data/lib/onetime.rb +0 -33
data/lib/onetime/transport.rb
DELETED
|
@@ -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
|
data/lib/onetime/version.rb
DELETED
|
@@ -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
|