flashpi 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/CHANGELOG.md +25 -0
- data/LICENSE +21 -0
- data/README.en.md +379 -0
- data/README.md +383 -0
- data/lib/flashpi/api_response.rb +33 -0
- data/lib/flashpi/client.rb +112 -0
- data/lib/flashpi/errors.rb +116 -0
- data/lib/flashpi/internal/engine.rb +355 -0
- data/lib/flashpi/internal/json_text.rb +108 -0
- data/lib/flashpi/internal/multipart.rb +141 -0
- data/lib/flashpi/internal/problems.rb +111 -0
- data/lib/flashpi/internal/request.rb +231 -0
- data/lib/flashpi/internal/resource.rb +125 -0
- data/lib/flashpi/internal/secret.rb +41 -0
- data/lib/flashpi/internal/settings.rb +88 -0
- data/lib/flashpi/internal/transport.rb +432 -0
- data/lib/flashpi/internal/validate.rb +122 -0
- data/lib/flashpi/page.rb +82 -0
- data/lib/flashpi/resources/documents.rb +136 -0
- data/lib/flashpi/resources/events.rb +36 -0
- data/lib/flashpi/resources/issuer_onboardings.rb +47 -0
- data/lib/flashpi/resources/issuers.rb +179 -0
- data/lib/flashpi/resources/webhook_endpoints.rb +110 -0
- data/lib/flashpi/version.rb +6 -0
- data/lib/flashpi/webhooks.rb +223 -0
- data/lib/flashpi.rb +42 -0
- data/sig/flashpi/internal.rbs +376 -0
- data/sig/flashpi/resources.rbs +712 -0
- data/sig/flashpi/sdk_types.rbs +114 -0
- data/sig/flashpi/types.rbs +2495 -0
- data/sig/flashpi.rbs +187 -0
- data/sig/manifest.yaml +8 -0
- metadata +80 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flashpi
|
|
4
|
+
# Verification of the webhooks Flashpi sends (Standard Webhooks). It needs no API key, and is
|
|
5
|
+
# also available as `client.webhooks`.
|
|
6
|
+
#
|
|
7
|
+
# A webhook is signed with HMAC-SHA256 over `webhook-id.webhook-timestamp.body` using the
|
|
8
|
+
# endpoint's `whsec_` secret. During a secret rotation Flashpi signs with both secrets, so the
|
|
9
|
+
# `webhook-signature` header lists several signatures.
|
|
10
|
+
#
|
|
11
|
+
# event = Flashpi::Webhooks.unwrap(request.raw_post, request.headers, ENV.fetch("FLASHPI_WEBHOOK_SECRET"))
|
|
12
|
+
# case event[:type]
|
|
13
|
+
# when "webhook_endpoint.verification" then render json: { challenge: event[:data][:challenge] }
|
|
14
|
+
# when "invoice.authorized" then ...
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Error trackers can record the local variables of the method that raises, so every
|
|
18
|
+
# verification error is raised by a method that holds neither the secret, its key nor the
|
|
19
|
+
# expected signature.
|
|
20
|
+
module Webhooks
|
|
21
|
+
# Default largest gap, in seconds, between a webhook's timestamp and the receiver's clock.
|
|
22
|
+
TOLERANCE_SECONDS = 300
|
|
23
|
+
|
|
24
|
+
SECRET_PREFIX = "whsec_"
|
|
25
|
+
# Key sizes of a Standard Webhooks secret (Flashpi makes 32-byte keys).
|
|
26
|
+
MIN_SECRET_BYTES = 24
|
|
27
|
+
MAX_SECRET_BYTES = 64
|
|
28
|
+
SIGNATURE_BYTES = 32
|
|
29
|
+
TIMESTAMP = /\A[0-9]{1,12}\z/
|
|
30
|
+
BASE64 = %r{\A[A-Za-z0-9+/]*={0,2}\z}
|
|
31
|
+
NO_MATCH = "No webhook-signature matches the body and the secret"
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Checks that a webhook comes from Flashpi and is recent.
|
|
35
|
+
#
|
|
36
|
+
# Raises a {WebhookVerificationError} otherwise: answer 400 and ignore the request. Pass the
|
|
37
|
+
# raw body exactly as received; a body parsed and serialized again does not match.
|
|
38
|
+
#
|
|
39
|
+
# @param payload [String] the raw body
|
|
40
|
+
# @param headers [Hash, #each] the request headers: a Hash in any letter case, a Rack env
|
|
41
|
+
# (`HTTP_WEBHOOK_ID`) or Rails' `request.headers`; repeated headers may come as Arrays
|
|
42
|
+
# @param secret [String] the endpoint's signing secret (`whsec_...`)
|
|
43
|
+
# @param tolerance [Integer, Float, nil] largest gap, in seconds, between
|
|
44
|
+
# `webhook-timestamp` and now (default 300)
|
|
45
|
+
# @param now [Time, Integer, Float, nil] the current time, for tests: a Time or Unix seconds
|
|
46
|
+
# @return [nil]
|
|
47
|
+
# @raise [WebhookVerificationError]
|
|
48
|
+
# @raise [TypeError, ArgumentError] for a bad `tolerance` or `now`
|
|
49
|
+
def verify_signature(payload, headers, secret, tolerance: nil, now: nil)
|
|
50
|
+
tolerance_seconds, now_seconds = verify_options(tolerance, now)
|
|
51
|
+
key = signing_key(secret)
|
|
52
|
+
webhook_id = header(headers, "webhook-id")
|
|
53
|
+
timestamp = header(headers, "webhook-timestamp")
|
|
54
|
+
signatures = header(headers, "webhook-signature")
|
|
55
|
+
if webhook_id.nil? || timestamp.nil? || signatures.nil?
|
|
56
|
+
reject!(:missing_headers, "The webhook-id, webhook-timestamp and webhook-signature headers are required")
|
|
57
|
+
end
|
|
58
|
+
reject!(:invalid_timestamp, "The webhook-timestamp is not valid") unless TIMESTAMP.match?(timestamp)
|
|
59
|
+
# Written so that anything unexpected fails closed.
|
|
60
|
+
unless (now_seconds.floor - timestamp.to_i).abs <= tolerance_seconds
|
|
61
|
+
reject!(:timestamp_out_of_range,
|
|
62
|
+
"The webhook-timestamp is more than #{shown(tolerance_seconds)} seconds away from now")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
body = payload_bytes(payload)
|
|
66
|
+
reject!(:no_matching_signature, NO_MATCH) unless signed?(key, "#{webhook_id.b}.#{timestamp}.".b + body,
|
|
67
|
+
signatures)
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Verifies a webhook (see {verify_signature}) and returns its parsed body, with Symbol keys.
|
|
72
|
+
#
|
|
73
|
+
# Branch on `event[:type]`. Endpoints in thin mode receive no `data`: read the event with
|
|
74
|
+
# `client.events.get(event[:id])`.
|
|
75
|
+
#
|
|
76
|
+
# @return [Hash{Symbol => Object}] the event
|
|
77
|
+
# @raise [WebhookVerificationError]
|
|
78
|
+
def unwrap(payload, headers, secret, tolerance: nil, now: nil)
|
|
79
|
+
verify_signature(payload, headers, secret, tolerance:, now:)
|
|
80
|
+
event = parse(payload)
|
|
81
|
+
reject!(:invalid_payload, "The webhook body is not a JSON object") unless event.is_a?(Hash)
|
|
82
|
+
|
|
83
|
+
_ = event
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def parse(payload)
|
|
89
|
+
Internal::JSONText.parse(Internal::JSONText.decode(payload.b))
|
|
90
|
+
rescue Internal::JSONText::Invalid
|
|
91
|
+
reject!(:invalid_payload, "The webhook body is not JSON")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Raises from a frame that holds only the reason.
|
|
95
|
+
def reject!(reason, message)
|
|
96
|
+
raise WebhookVerificationError.new(reason, message)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# A header without letter case, or in a Rack env's form (`HTTP_WEBHOOK_ID`). Repeated
|
|
100
|
+
# headers: signature lists are joined, otherwise the first. An empty value is missing. Names
|
|
101
|
+
# and values are read as bytes (as Rack servers give them), so no encoding of theirs can
|
|
102
|
+
# make the checks below raise anything but a WebhookVerificationError.
|
|
103
|
+
def header(headers, name)
|
|
104
|
+
unless headers.respond_to?(:each)
|
|
105
|
+
raise TypeError, "headers must be a Hash (or Rack env, or Rails request.headers), not #{headers.class}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
env_name = "HTTP_#{name.upcase.tr("-", "_")}"
|
|
109
|
+
values = [] #: Array[String]
|
|
110
|
+
headers.each do |key, value|
|
|
111
|
+
next unless key.is_a?(String) || key.is_a?(Symbol)
|
|
112
|
+
|
|
113
|
+
field = key.to_s.b
|
|
114
|
+
next unless field.casecmp?(name) || field == env_name
|
|
115
|
+
|
|
116
|
+
values << value.b if value.is_a?(String)
|
|
117
|
+
values.concat(value.grep(String).map(&:b)) if value.is_a?(Array)
|
|
118
|
+
end
|
|
119
|
+
found = name == "webhook-signature" ? values.join(" ") : values.first
|
|
120
|
+
found.nil? || found.empty? ? nil : found
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# The decoded key of a `whsec_` secret, in a {Internal::Secret}.
|
|
124
|
+
def signing_key(secret)
|
|
125
|
+
unless secret.is_a?(String) && !secret.b.strip.empty?
|
|
126
|
+
reject!(:invalid_secret,
|
|
127
|
+
"The webhook secret is empty: copy it from the console or webhook_endpoints.get_secret")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
size = key_size(secret)
|
|
131
|
+
if size.zero?
|
|
132
|
+
reject!(:invalid_secret, "The webhook secret is not a whsec_ secret (base64 after the prefix)")
|
|
133
|
+
elsif size < MIN_SECRET_BYTES || size > MAX_SECRET_BYTES
|
|
134
|
+
reject!(:invalid_secret, "The webhook secret is not a complete whsec_ secret: copy it again from the " \
|
|
135
|
+
"console or webhook_endpoints.get_secret")
|
|
136
|
+
end
|
|
137
|
+
Internal::Secret.new(decode_base64(encoded_key(secret)) || "")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# The size of a secret's key, or 0 when it is not base64. It returns instead of raising, so
|
|
141
|
+
# the key never stays in the frame of an error.
|
|
142
|
+
def key_size(secret)
|
|
143
|
+
key = decode_base64(encoded_key(secret))
|
|
144
|
+
key ? key.bytesize : 0
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# The base64 after the prefix, read as bytes.
|
|
148
|
+
def encoded_key(secret)
|
|
149
|
+
secret.b.strip.delete_prefix(SECRET_PREFIX)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Whether a v1 signature of the header signs `message`. The signatures are read before the
|
|
153
|
+
# expected one is computed, and nothing after that can raise, so the expected signature
|
|
154
|
+
# never stays in the frame of an error.
|
|
155
|
+
def signed?(key, message, signatures)
|
|
156
|
+
candidates = v1_signatures(signatures)
|
|
157
|
+
expected = OpenSSL::HMAC.digest("SHA256", key.reveal, message)
|
|
158
|
+
candidates.any? { |signature| OpenSSL.fixed_length_secure_compare(signature, expected) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The decoded v1 signatures of a `webhook-signature` header that have the right size.
|
|
162
|
+
def v1_signatures(signatures)
|
|
163
|
+
signatures.b.split(/ /).filter_map do |entry|
|
|
164
|
+
version, comma, encoded = entry.partition(",")
|
|
165
|
+
next if comma.empty? || version != "v1"
|
|
166
|
+
|
|
167
|
+
signature = decode_base64(encoded)
|
|
168
|
+
signature if signature && signature.bytesize == SIGNATURE_BYTES
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Standard base64 to bytes, or nil when it is not base64 (padding optional, like atob).
|
|
173
|
+
def decode_base64(value)
|
|
174
|
+
return nil unless BASE64.match?(value) && value.length % 4 != 1
|
|
175
|
+
|
|
176
|
+
data = (value.length % 4).zero? ? value.sub(/=+\z/, "") : value
|
|
177
|
+
return nil if data.include?("=") || data.length % 4 == 1
|
|
178
|
+
|
|
179
|
+
# Only the alphabet is left, in a length that decodes once padded.
|
|
180
|
+
"#{data}#{"=" * (-data.length % 4)}".unpack1("m").to_s
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def payload_bytes(payload)
|
|
184
|
+
unless payload.is_a?(String)
|
|
185
|
+
reject!(:invalid_payload,
|
|
186
|
+
"Pass the raw webhook body as a String (not parsed JSON)")
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
payload.b
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# The checked tolerance and clock. Bad values are programming errors: TypeError or
|
|
193
|
+
# ArgumentError, never a skipped time check.
|
|
194
|
+
def verify_options(tolerance, now)
|
|
195
|
+
value = tolerance.nil? ? TOLERANCE_SECONDS : tolerance
|
|
196
|
+
unless value.is_a?(Integer) || value.is_a?(Float)
|
|
197
|
+
raise TypeError,
|
|
198
|
+
"tolerance must be a number of seconds, 0 or more"
|
|
199
|
+
end
|
|
200
|
+
raise ArgumentError, "tolerance must be a number of seconds, 0 or more" unless value.to_f.finite? && value >= 0
|
|
201
|
+
|
|
202
|
+
[value.to_f, clock(now)]
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def clock(now)
|
|
206
|
+
seconds = case now
|
|
207
|
+
when nil then Time.now.to_f
|
|
208
|
+
when Time then now.to_f
|
|
209
|
+
when Integer then now.bit_length > 1000 ? Float::INFINITY : now.to_f
|
|
210
|
+
when Float then now
|
|
211
|
+
else raise TypeError, "now must be a Time or a number of Unix seconds"
|
|
212
|
+
end
|
|
213
|
+
raise ArgumentError, "now must be a Time or a number of Unix seconds" unless seconds.finite?
|
|
214
|
+
|
|
215
|
+
seconds
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def shown(seconds)
|
|
219
|
+
seconds == seconds.floor ? seconds.to_i.to_s : seconds.to_s
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
data/lib/flashpi.rb
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ipaddr"
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "openssl"
|
|
7
|
+
require "securerandom"
|
|
8
|
+
require "time"
|
|
9
|
+
require "uri"
|
|
10
|
+
require "zlib"
|
|
11
|
+
|
|
12
|
+
require_relative "flashpi/version"
|
|
13
|
+
require_relative "flashpi/errors"
|
|
14
|
+
require_relative "flashpi/api_response"
|
|
15
|
+
require_relative "flashpi/page"
|
|
16
|
+
require_relative "flashpi/internal/secret"
|
|
17
|
+
require_relative "flashpi/internal/validate"
|
|
18
|
+
require_relative "flashpi/internal/json_text"
|
|
19
|
+
require_relative "flashpi/internal/problems"
|
|
20
|
+
require_relative "flashpi/internal/request"
|
|
21
|
+
require_relative "flashpi/internal/multipart"
|
|
22
|
+
require_relative "flashpi/internal/transport"
|
|
23
|
+
require_relative "flashpi/internal/engine"
|
|
24
|
+
require_relative "flashpi/internal/resource"
|
|
25
|
+
require_relative "flashpi/internal/settings"
|
|
26
|
+
require_relative "flashpi/resources/documents"
|
|
27
|
+
require_relative "flashpi/resources/issuers"
|
|
28
|
+
require_relative "flashpi/resources/issuer_onboardings"
|
|
29
|
+
require_relative "flashpi/resources/webhook_endpoints"
|
|
30
|
+
require_relative "flashpi/resources/events"
|
|
31
|
+
require_relative "flashpi/webhooks"
|
|
32
|
+
require_relative "flashpi/client"
|
|
33
|
+
|
|
34
|
+
# The official Ruby SDK for Flashpi, the electronic invoicing API for Ecuador's SRI.
|
|
35
|
+
#
|
|
36
|
+
# require "flashpi"
|
|
37
|
+
#
|
|
38
|
+
# flashpi = Flashpi::Client.new # reads FLASHPI_API_KEY
|
|
39
|
+
# invoice = flashpi.invoices.create({ issuerId: "...", ... })
|
|
40
|
+
# invoice[:accessKey]
|
|
41
|
+
module Flashpi
|
|
42
|
+
end
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# The SDK's internals: not part of its API, and free to change in any release.
|
|
2
|
+
module Flashpi
|
|
3
|
+
module Internal
|
|
4
|
+
class Secret[out T]
|
|
5
|
+
@reader: ^() -> T
|
|
6
|
+
|
|
7
|
+
def initialize: (T value) -> void
|
|
8
|
+
def reveal: () -> T
|
|
9
|
+
def marshal_dump: () -> bot
|
|
10
|
+
def _dump: (Integer level) -> bot
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
module Validate
|
|
14
|
+
MAX_TIMEOUT: Float
|
|
15
|
+
TOKEN: Regexp
|
|
16
|
+
AROUND: Regexp
|
|
17
|
+
CONTROL: Regexp
|
|
18
|
+
FRAMING: Array[String]
|
|
19
|
+
|
|
20
|
+
def self?.timeout!: (String name, untyped value) -> Float
|
|
21
|
+
def self?.retries!: (String name, untyped value) -> Integer
|
|
22
|
+
def self?.header_name!: (untyped name) -> String
|
|
23
|
+
def self?.header_value!: (String label, untyped value) -> String
|
|
24
|
+
def self?.headers!: (String label, untyped headers) -> Hash[String, String]
|
|
25
|
+
def self?.utf8: (String text) -> String?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module JSONText
|
|
29
|
+
class Invalid < StandardError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
MAX_NESTING: Integer
|
|
33
|
+
|
|
34
|
+
def self?.encode: (untyped body) -> String
|
|
35
|
+
def self?.parse: (String text) -> untyped
|
|
36
|
+
def self?.decode: (String bytes) -> String
|
|
37
|
+
def self?.plain: (untyped value, Hash[Integer, bool] seen) -> untyped
|
|
38
|
+
def self?.plain_hash: (Hash[untyped, untyped] hash, Hash[Integer, bool] seen) -> Hash[String, untyped]
|
|
39
|
+
def self?.key_text: (untyped key) -> String
|
|
40
|
+
def self?.nested: [R] (Object container, Hash[Integer, bool] seen) { () -> R } -> R
|
|
41
|
+
def self?.text: (String value) -> String
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
module Problems
|
|
45
|
+
ERROR_CLASSES: Hash[Integer, singleton(APIError)]
|
|
46
|
+
DELTA_SECONDS: Regexp
|
|
47
|
+
MAX_DELTA_DIGITS: Integer
|
|
48
|
+
MAX_DELTA: Integer
|
|
49
|
+
TIME_ZONE: Regexp
|
|
50
|
+
|
|
51
|
+
def self?.error: (Integer status, Hash[String, String] headers, String body_text) -> APIError
|
|
52
|
+
def self?.parse: (String body_text) -> Types::problem?
|
|
53
|
+
def self?.message: (Integer status, Types::problem? problem, String body_text) -> String
|
|
54
|
+
def self?.text: (untyped value) -> String?
|
|
55
|
+
def self?.retry_after: (String? value, ?Float now) -> Integer?
|
|
56
|
+
def self?.date: (String value) -> Float?
|
|
57
|
+
def self?.iso8601: (String value) -> Float?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
class ClientConfig
|
|
61
|
+
@api_key: Secret[String]
|
|
62
|
+
|
|
63
|
+
attr_reader base_url: String
|
|
64
|
+
attr_reader timeout: Float
|
|
65
|
+
attr_reader max_retries: Integer
|
|
66
|
+
attr_reader language: String?
|
|
67
|
+
attr_reader default_headers: Hash[String, String]
|
|
68
|
+
|
|
69
|
+
def initialize: (
|
|
70
|
+
api_key: String, base_url: String, timeout: Float, max_retries: Integer, language: String?,
|
|
71
|
+
default_headers: Hash[String, String]
|
|
72
|
+
) -> void
|
|
73
|
+
def api_key: () -> String
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
type http_method = :get | :post | :patch | :delete
|
|
77
|
+
type response_type = :json | :text | :bytes | :none
|
|
78
|
+
|
|
79
|
+
class RequestSpec
|
|
80
|
+
attr_reader http_method: http_method
|
|
81
|
+
attr_reader path: String
|
|
82
|
+
attr_reader query: Hash[untyped, untyped]?
|
|
83
|
+
attr_reader body: untyped
|
|
84
|
+
attr_reader multipart: Multipart?
|
|
85
|
+
attr_reader accept: String
|
|
86
|
+
attr_reader response_type: response_type
|
|
87
|
+
attr_reader options: Hash[Symbol, untyped]
|
|
88
|
+
attr_reader min_timeout: Float
|
|
89
|
+
attr_reader recover_document: bool
|
|
90
|
+
|
|
91
|
+
def initialize: (
|
|
92
|
+
http_method: http_method, path: String, ?query: Hash[untyped, untyped]?, ?body: untyped,
|
|
93
|
+
?multipart: Multipart?, ?accept: String, ?response_type: response_type,
|
|
94
|
+
?options: Hash[Symbol, untyped], ?min_timeout: Float, ?recover_document: bool
|
|
95
|
+
) -> void
|
|
96
|
+
def with_query: (Hash[untyped, untyped] query) -> RequestSpec
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Attempt
|
|
100
|
+
@headers: Secret[Hash[String, String]]
|
|
101
|
+
@content: Secret[String?]
|
|
102
|
+
|
|
103
|
+
attr_reader http_method: http_method
|
|
104
|
+
attr_reader url: String
|
|
105
|
+
attr_reader target: String
|
|
106
|
+
attr_reader timeout: Float
|
|
107
|
+
attr_reader response_type: response_type
|
|
108
|
+
|
|
109
|
+
def initialize: (
|
|
110
|
+
http_method: http_method, url: String, target: String, headers: Hash[String, String],
|
|
111
|
+
content: String?, timeout: Float, response_type: response_type
|
|
112
|
+
) -> void
|
|
113
|
+
def headers: () -> Hash[String, String]
|
|
114
|
+
def content: () -> String?
|
|
115
|
+
def verb: () -> String
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
class RawResponse
|
|
119
|
+
attr_reader status: Integer
|
|
120
|
+
attr_reader headers: Hash[String, String]
|
|
121
|
+
attr_reader body: String
|
|
122
|
+
|
|
123
|
+
def initialize: (Integer status, Hash[String, String] headers, String body) -> void
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
module Path
|
|
127
|
+
UNRESERVED: Regexp
|
|
128
|
+
BLANK: Regexp
|
|
129
|
+
|
|
130
|
+
def self?.of: (String template, *untyped ids) -> String
|
|
131
|
+
def self?.segment: (untyped id, String shown) -> String
|
|
132
|
+
def self?.encode: (String text) -> String
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
interface _Clock
|
|
136
|
+
def now: () -> Float
|
|
137
|
+
def sleep: (Float seconds) -> untyped
|
|
138
|
+
def rand: () -> Float
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
module Clock
|
|
142
|
+
def self?.now: () -> Float
|
|
143
|
+
def self?.sleep: (Float seconds) -> untyped
|
|
144
|
+
def self?.rand: () -> Float
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class Multipart
|
|
148
|
+
MAX_CERTIFICATE_BYTES: Integer
|
|
149
|
+
DEFAULT_FILENAME: String
|
|
150
|
+
CONTENT_TYPE: String
|
|
151
|
+
PIECE: Integer
|
|
152
|
+
|
|
153
|
+
@body: Secret[String]
|
|
154
|
+
|
|
155
|
+
attr_reader content_type: String
|
|
156
|
+
|
|
157
|
+
def self.certificate: (untyped upload) -> Multipart
|
|
158
|
+
def self.member: (Hash[untyped, untyped] upload, Symbol name) -> untyped
|
|
159
|
+
def self.read: (untyped file) -> String
|
|
160
|
+
def self.read_io: (untyped file) -> String
|
|
161
|
+
def self.read_piece: (untyped file, Integer length) -> untyped
|
|
162
|
+
def self.refuse!: (singleton(Exception) error, String message) -> bot
|
|
163
|
+
|
|
164
|
+
def initialize: (String password, String content, String filename) -> void
|
|
165
|
+
def content: () -> String
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def encode: (String boundary, String password, String content, String filename) -> String
|
|
170
|
+
def escape: (String filename) -> String
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
class Deadline
|
|
174
|
+
@clock: _Clock
|
|
175
|
+
@end: Float
|
|
176
|
+
|
|
177
|
+
def initialize: (Float seconds, ?_Clock clock) -> void
|
|
178
|
+
def left: () -> Float
|
|
179
|
+
def passed?: () -> bool
|
|
180
|
+
def left!: (singleton(Exception) error) -> Float
|
|
181
|
+
def limit: (Float? timeout, singleton(Exception) error) -> Float
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
module DeadlineHTTP : Net::HTTP
|
|
185
|
+
@socket: untyped
|
|
186
|
+
|
|
187
|
+
attr_accessor flashpi_deadline: Deadline?
|
|
188
|
+
|
|
189
|
+
private
|
|
190
|
+
|
|
191
|
+
def connect: () -> void
|
|
192
|
+
def ssl_socket_connect: (OpenSSL::SSL::SSLSocket socket, Float? timeout) -> void
|
|
193
|
+
def on_connect: () -> void
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
class DeadlineSocket
|
|
197
|
+
@http: DeadlineHTTP
|
|
198
|
+
|
|
199
|
+
attr_reader io: untyped
|
|
200
|
+
|
|
201
|
+
def initialize: (untyped io, DeadlineHTTP http) -> void
|
|
202
|
+
def read_nonblock: (Integer length, ?String? buffer, ?exception: bool) -> (String | Symbol | nil)
|
|
203
|
+
def write_nonblock: (String data, ?exception: bool) -> (Integer | Symbol)
|
|
204
|
+
def to_io: () -> self
|
|
205
|
+
def wait_readable: (?Float? timeout) -> untyped
|
|
206
|
+
def wait_writable: (?Float? timeout) -> untyped
|
|
207
|
+
def close: () -> untyped
|
|
208
|
+
def closed?: () -> bool
|
|
209
|
+
def eof?: () -> bool
|
|
210
|
+
def respond_to_missing?: (Symbol name, ?bool include_private) -> bool
|
|
211
|
+
def method_missing: (Symbol name, *untyped args, **untyped options) ?{ (*untyped) -> untyped } -> untyped
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def deadline: () -> Deadline?
|
|
216
|
+
def limited: (Float? timeout, singleton(Exception) error) -> Float?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
module Proxy
|
|
220
|
+
LOOPBACK: Regexp
|
|
221
|
+
|
|
222
|
+
# The environment, or a Hash for the tests.
|
|
223
|
+
interface _Env
|
|
224
|
+
def []: (String name) -> String?
|
|
225
|
+
def key?: (String name) -> bool
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def self?.from_env: (URI::Generic uri, ?_Env env) -> [String, Integer, String?, String?]?
|
|
229
|
+
def self?.bypassed?: (URI::Generic uri, String? no_proxy) -> bool
|
|
230
|
+
def self?.parse: (String name, String value) -> [String, Integer, String?, String?]
|
|
231
|
+
def self?.refuse!: (String name) -> bot
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
class Transport
|
|
235
|
+
MAX_IDLE: Integer
|
|
236
|
+
CONNECTION_ERRORS: Array[singleton(Exception)]
|
|
237
|
+
TIMEOUT_ERRORS: Array[singleton(Exception)]
|
|
238
|
+
REQUESTS: Hash[Symbol, singleton(Net::HTTP::Get) | singleton(Net::HTTP::Post) | singleton(Net::HTTP::Patch) | singleton(Net::HTTP::Delete)]
|
|
239
|
+
|
|
240
|
+
@host: String
|
|
241
|
+
@port: Integer
|
|
242
|
+
@use_ssl: bool
|
|
243
|
+
@proxy: [String, Integer, String?, String?]?
|
|
244
|
+
@clock: _Clock
|
|
245
|
+
@idle: Array[Net::HTTP & DeadlineHTTP]
|
|
246
|
+
@mutex: Thread::Mutex
|
|
247
|
+
@pid: Integer
|
|
248
|
+
|
|
249
|
+
def initialize: (String base_url, ?_Clock clock) -> void
|
|
250
|
+
def perform: (Attempt attempt) -> RawResponse
|
|
251
|
+
def close: () -> nil
|
|
252
|
+
|
|
253
|
+
private
|
|
254
|
+
|
|
255
|
+
def exchange: (Net::HTTP & DeadlineHTTP http, Attempt attempt, Deadline deadline) -> RawResponse
|
|
256
|
+
def gzip_inflater: (Net::HTTPResponse response) -> Zlib::Inflate?
|
|
257
|
+
def limit_timeouts: (Net::HTTP http, Deadline deadline) -> void
|
|
258
|
+
def build_request: (Attempt attempt) -> Net::HTTPRequest
|
|
259
|
+
def checkout: () -> (Net::HTTP & DeadlineHTTP)
|
|
260
|
+
def checkin: (Net::HTTP & DeadlineHTTP http) -> void
|
|
261
|
+
def discard: (Net::HTTP http) -> void
|
|
262
|
+
def connect: () -> (Net::HTTP & DeadlineHTTP)
|
|
263
|
+
def forked?: () -> bool
|
|
264
|
+
def seconds: (Float value) -> String
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
class Engine
|
|
268
|
+
USER_AGENT: String
|
|
269
|
+
MAX_RETRY_AFTER: Integer
|
|
270
|
+
INITIAL_RETRY_DELAY: Float
|
|
271
|
+
MAX_RETRY_DELAY: Float
|
|
272
|
+
IN_PROGRESS_FIRST_WAIT: Float
|
|
273
|
+
IN_PROGRESS_MAX_WAIT: Float
|
|
274
|
+
IN_PROGRESS: String
|
|
275
|
+
OPTION_NAMES: Array[Symbol]
|
|
276
|
+
|
|
277
|
+
class Call
|
|
278
|
+
@content: Secret[String?]
|
|
279
|
+
|
|
280
|
+
attr_reader options: Hash[Symbol, untyped]
|
|
281
|
+
attr_reader max_retries: Integer
|
|
282
|
+
attr_reader timeout: Float
|
|
283
|
+
attr_reader url: String
|
|
284
|
+
attr_reader target: String
|
|
285
|
+
attr_reader idempotency_key: String?
|
|
286
|
+
|
|
287
|
+
def initialize: (
|
|
288
|
+
options: Hash[Symbol, untyped], max_retries: Integer, timeout: Float, url: String, target: String,
|
|
289
|
+
idempotency_key: String?, content: String?
|
|
290
|
+
) -> void
|
|
291
|
+
def content: () -> String?
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
@base_path: String
|
|
295
|
+
|
|
296
|
+
attr_reader config: ClientConfig
|
|
297
|
+
attr_accessor transport: Transport
|
|
298
|
+
attr_accessor clock: _Clock
|
|
299
|
+
|
|
300
|
+
def initialize: (ClientConfig config, Transport transport, ?_Clock clock) -> void
|
|
301
|
+
def request: (RequestSpec spec) -> APIResponse[untyped]
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def unanswered?: (Error failure) -> bool
|
|
306
|
+
def attempt_once: (RequestSpec spec, Call call) -> (APIResponse[untyped] | APIError | APIConnectionError)
|
|
307
|
+
def prepare: (RequestSpec spec) -> Call
|
|
308
|
+
def body_content: (RequestSpec spec) -> String?
|
|
309
|
+
def attempt_of: (RequestSpec spec, Call call) -> Attempt
|
|
310
|
+
def recover_document: (Call call, String document_id) -> APIResponse[untyped]
|
|
311
|
+
def retry_delay: (Error failure, Integer retries) -> Float?
|
|
312
|
+
def backoff: (Integer retries) -> Float
|
|
313
|
+
def recoverable_document: (RequestSpec spec, APIError failure) -> String?
|
|
314
|
+
def deleted: (APIError failure) -> APIResponse[nil]
|
|
315
|
+
def check_options: (Hash[Symbol, untyped] options) -> Hash[Symbol, untyped]
|
|
316
|
+
def language: (String label, untyped value) -> String
|
|
317
|
+
def idempotency_key_of: (http_method http_method, Hash[Symbol, untyped] options) -> String?
|
|
318
|
+
def build_target: (String path, Hash[untyped, untyped]? query) -> String
|
|
319
|
+
def query_name: (untyped name) -> String
|
|
320
|
+
def query_value: (untyped value) -> String
|
|
321
|
+
def query_text: (String value) -> String
|
|
322
|
+
def build_headers: (RequestSpec spec, Call call) -> Hash[String, String]
|
|
323
|
+
def read_response: (RawResponse raw, response_type response_type) -> (APIResponse[untyped] | APIError)
|
|
324
|
+
def read_data: (RawResponse raw, response_type response_type) -> untyped
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
class Resource
|
|
328
|
+
@engine: Engine
|
|
329
|
+
@with_response: ResponseView?
|
|
330
|
+
|
|
331
|
+
def self.response_methods: (*Symbol names) -> void
|
|
332
|
+
|
|
333
|
+
def initialize: (Engine engine) -> void
|
|
334
|
+
def with_response: () -> ResponseView
|
|
335
|
+
|
|
336
|
+
private
|
|
337
|
+
|
|
338
|
+
def operation: (http_method http_method, String template, *untyped ids, **untyped settings) -> RequestSpec
|
|
339
|
+
def send_request: (RequestSpec spec) -> untyped
|
|
340
|
+
def send_call: (RequestSpec spec) -> APIResponse[untyped]
|
|
341
|
+
def paginate: (RequestSpec spec) -> Page[untyped]
|
|
342
|
+
def fetch_page: (RequestSpec spec, RequestSpec page_spec) -> APIResponse[Page[untyped]]
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
class ResponseView
|
|
346
|
+
SLOT: Symbol
|
|
347
|
+
|
|
348
|
+
@resource: Resource
|
|
349
|
+
|
|
350
|
+
def self.record: (APIResponse[untyped] result) -> void
|
|
351
|
+
|
|
352
|
+
def initialize: (Resource resource) -> void
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
attr_reader resource: Resource
|
|
357
|
+
|
|
358
|
+
def capture: [T] () { () -> T } -> APIResponse[T]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
module Settings
|
|
362
|
+
API_KEY_CHARACTERS: Regexp
|
|
363
|
+
JS_SPACE: String
|
|
364
|
+
AROUND: Regexp
|
|
365
|
+
|
|
366
|
+
def self?.configure: (
|
|
367
|
+
untyped api_key, base_url: untyped, timeout: untyped, max_retries: untyped, language: untyped,
|
|
368
|
+
default_headers: untyped
|
|
369
|
+
) -> ClientConfig
|
|
370
|
+
def self?.refuse!: (String message) -> bot
|
|
371
|
+
def self?.http_url?: (untyped url) -> bool
|
|
372
|
+
def self?.trimmed_key: (untyped api_key) -> String
|
|
373
|
+
def self?.key_problem: (String key) -> String?
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|