synthient 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.
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # Decoding is strict: a field the API schema requires must be present,
5
+ # non-null, and of its declared type. Optional fields may be absent or null
6
+ # and become nil. Unknown fields are ignored so that additive API changes
7
+ # don't break decoding.
8
+ #
9
+ # @api private
10
+ module Model
11
+ Field = Data.define(:name, :wire, :type, :required)
12
+
13
+ class Builder
14
+ attr_reader :fields
15
+
16
+ def initialize
17
+ @fields = []
18
+ end
19
+
20
+ def required(name, type, wire: name.to_s)
21
+ @fields << Field.new(name:, wire:, type:, required: true)
22
+ end
23
+
24
+ def optional(name, type, wire: name.to_s)
25
+ @fields << Field.new(name:, wire:, type:, required: false)
26
+ end
27
+ end
28
+
29
+ def self.define(&)
30
+ builder = Builder.new
31
+ builder.instance_eval(&)
32
+ fields = builder.fields.freeze
33
+ defaults = fields.reject(&:required).to_h { |field| [field.name, nil] }.freeze
34
+ Data.define(*fields.map(&:name)) do
35
+ define_singleton_method(:fields) { fields }
36
+ define_method(:initialize) { |**attrs| super(**defaults, **attrs) }
37
+ end
38
+ end
39
+
40
+ def self.decode(model, object, path = model.name.delete_prefix("Synthient::"))
41
+ raise DecodeError, "#{path} must be an object" unless object.is_a?(Hash)
42
+
43
+ attrs = model.fields.to_h do |field|
44
+ value = object[field.wire]
45
+ field_path = "#{path}.#{field.wire}"
46
+ raise DecodeError, "#{field_path} is required" if value.nil? && field.required
47
+
48
+ [field.name, value.nil? ? nil : decode_value(field.type, value, field_path)]
49
+ end
50
+ model.new(**attrs)
51
+ end
52
+
53
+ def self.decode_value(type, value, path)
54
+ case type
55
+ when :integer
56
+ return value if value.is_a?(Integer)
57
+ when :float
58
+ return value.to_f if (value.is_a?(Integer) || value.is_a?(Float)) && value.to_f.finite?
59
+ when :string
60
+ return value if value.is_a?(String)
61
+ when :bool
62
+ return value if [true, false].include?(value)
63
+ when :unix_time
64
+ if value.is_a?(Integer)
65
+ # The spec says seconds, but the Helios docs show milliseconds, so
66
+ # values from 1e12 up are read as milliseconds.
67
+ return Time.at(value, in: "UTC") if value < 1_000_000_000_000
68
+
69
+ return Time.at(value / 1000, value % 1000, :millisecond, in: "UTC")
70
+ end
71
+ when :time
72
+ return Time.iso8601(value) if value.is_a?(String)
73
+ when :date
74
+ return Date.iso8601(value) if value.is_a?(String) && value.match?(/\A\d{4}-\d{2}-\d{2}\z/)
75
+ when :bytes
76
+ return value.unpack1("m0").freeze if value.is_a?(String)
77
+ when :ip
78
+ return IPAddr.new(value) if value.is_a?(String) && !value.include?("/") && !value.include?("%")
79
+ when Array
80
+ if value.is_a?(Array)
81
+ return value.each_with_index.map { |item, index| decode_value(type.first, item, "#{path}[#{index}]") }.freeze
82
+ end
83
+ when Hash
84
+ return value.transform_values { |item| decode_value(type.values.first, item, path) }.freeze if value.is_a?(Hash)
85
+ else
86
+ return decode(type, value, path)
87
+ end
88
+ raise DecodeError, "#{path} is not a valid #{type.is_a?(Symbol) ? type.to_s.tr("_", " ") : type.class.name.downcase}"
89
+ rescue ArgumentError
90
+ raise DecodeError, "#{path} is not a valid #{type.to_s.tr("_", " ")}"
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # A member's role in an organization.
5
+ module Relation
6
+ OWNER = "OWNER"
7
+ ADMIN = "ADMIN"
8
+ MEMBER = "MEMBER"
9
+ end
10
+
11
+ # API key scopes. New values may be added.
12
+ module Scope
13
+ BASIC = "BASIC"
14
+ PROXY_FEEDS = "PROXY_FEEDS"
15
+ PROXY_FIREHOSE = "PROXY_FIREHOSE"
16
+ ANONYMIZERS_FEED = "ANONYMIZERS_FEED"
17
+ ANONYMIZERS_STREAM = "ANONYMIZERS_STREAM"
18
+ JA3_FEED = "JA3_FEED"
19
+ JA3_STREAM = "JA3_STREAM"
20
+ TORRENTS_FEED = "TORRENTS_FEED"
21
+ TORRENTS_STREAM = "TORRENTS_STREAM"
22
+ HONEYPOT_HTTP_FEED = "HONEYPOT_HTTP_FEED"
23
+ HONEYPOT_HTTP_STREAM = "HONEYPOT_HTTP_STREAM"
24
+ HONEYPOT_HTTPS_FEED = "HONEYPOT_HTTPS_FEED"
25
+ HONEYPOT_HTTPS_STREAM = "HONEYPOT_HTTPS_STREAM"
26
+ HONEYPOT_ADB_FEED = "HONEYPOT_ADB_FEED"
27
+ HONEYPOT_ADB_STREAM = "HONEYPOT_ADB_STREAM"
28
+ HONEYPOT_DNS_FEED = "HONEYPOT_DNS_FEED"
29
+ HONEYPOT_DNS_STREAM = "HONEYPOT_DNS_STREAM"
30
+ JA4_FEED = "JA4_FEED"
31
+ JA4_STREAM = "JA4_STREAM"
32
+ JA4T_FEED = "JA4T_FEED"
33
+ JA4T_STREAM = "JA4T_STREAM"
34
+ ADMIN_PROXIES = "ADMIN_PROXIES"
35
+ end
36
+
37
+ # +relation+ is the account's role, one of Relation.
38
+ Organization = Model.define do
39
+ required :id, :string
40
+ required :name, :string
41
+ required :relation, :string
42
+ end
43
+
44
+ # +resets_in+ is the number of seconds until the credits reset.
45
+ LookupQuota = Model.define do
46
+ required :credits, :integer
47
+ required :resets_in, :integer
48
+ end
49
+
50
+ # The API key that made the request.
51
+ AccountAPIKey = Model.define do
52
+ required :id, :string
53
+ required :name, :string
54
+ required :created_at, :time
55
+ end
56
+
57
+ # The account that owns the API key. +scopes+ holds Scope values.
58
+ Account = Model.define do
59
+ required :email, :string
60
+ required :first_name, :string
61
+ required :last_name, :string
62
+ required :organization, Organization
63
+ required :scopes, [:string]
64
+ required :lookup_quota, LookupQuota
65
+ optional :api_key, AccountAPIKey
66
+ end
67
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # Whether a domain had events in the last 24 hours.
5
+ module DomainStatus
6
+ ACTIVE = "active"
7
+ INACTIVE = "inactive"
8
+ end
9
+
10
+ # The sentences that describe a domain's activity over the last 30 days.
11
+ module Cadence
12
+ NO_ACTIVITY = "No activity in the last 30 days."
13
+ EVENLY_DISTRIBUTED = "Activity is evenly distributed across the day."
14
+ BUSINESS_HOURS = "Activity follows a weekday business-hours pattern."
15
+ WEEKEND = "Activity follows a weekend pattern."
16
+ BURST_DRIVEN = "Activity is burst-driven with significant peaks."
17
+ end
18
+
19
+ DomainStats = Model.define do
20
+ required :events_24h, :integer
21
+ required :total_events_30d, :integer
22
+ end
23
+
24
+ # Distinct source addresses seen for a domain. +sparkline_24h+ holds hourly
25
+ # counts for the last 24 hours, oldest first.
26
+ UniqueIPs = Model.define do
27
+ required :value_24h, :integer
28
+ required :value_30d, :integer
29
+ required :sparkline_24h, [:integer]
30
+ end
31
+
32
+ # The events and distinct addresses seen for a domain on one day.
33
+ TimeSeriesPoint = Model.define do
34
+ required :date, :unix_time
35
+ required :events, :integer
36
+ required :unique_ips, :integer
37
+ end
38
+
39
+ # The autonomous system that sent the most events.
40
+ ASNCount = Model.define do
41
+ required :asn, :integer
42
+ required :events, :integer
43
+ end
44
+
45
+ SubdomainCount = Model.define do
46
+ required :subdomain, :string
47
+ required :count, :integer
48
+ end
49
+
50
+ PortCount = Model.define do
51
+ required :port, :integer
52
+ required :count, :integer
53
+ end
54
+
55
+ # The events and distinct addresses seen from one country.
56
+ CountryCount = Model.define do
57
+ required :country_code, :string
58
+ required :events, :integer
59
+ required :unique_ips, :integer
60
+ end
61
+
62
+ # +peak_hour+ and +quiet_hour+ are UTC hours, and +cadence+ is one of
63
+ # Cadence.
64
+ ActivityStats = Model.define do
65
+ required :cadence, :string
66
+ required :median_per_hour, :integer
67
+ required :p95_per_hour, :integer
68
+ required :peak_hour, :integer
69
+ required :quiet_hour, :integer
70
+ end
71
+
72
+ # +source_ip+ is the full source address, although the API names it
73
+ # source_ip_masked.
74
+ DomainEvent = Model.define do
75
+ required :timestamp, :unix_time
76
+ required :source_ip, :string, wire: "source_ip_masked"
77
+ required :target_subdomain, :string
78
+ required :country_code, :string
79
+ required :port, :integer
80
+ end
81
+
82
+ # The honeypot traffic Synthient has observed for a domain. A domain that
83
+ # was never observed is +inactive+ with zero counts. +top_asn+ is nil when
84
+ # there were no events. +hour_dow_heatmap+ has 7 rows, Monday first, of 24
85
+ # UTC hourly counts.
86
+ DomainInfo = Model.define do
87
+ required :domain, :string
88
+ required :status, :string
89
+ required :stats, DomainStats
90
+ required :unique_ips, UniqueIPs
91
+ required :time_series, [TimeSeriesPoint]
92
+ optional :top_asn, ASNCount
93
+ required :top_subdomains, [SubdomainCount]
94
+ required :top_ports, [PortCount]
95
+ required :geo_distribution, [CountryCount]
96
+ required :hour_dow_heatmap, [[:integer]]
97
+ required :activity_stats, ActivityStats
98
+ required :recent_events, [DomainEvent]
99
+ end
100
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # The kinds of service seen on an address. New values may be added.
5
+ module ProxyType
6
+ FREE_VPN = "FREE_VPN"
7
+ COMMERCIAL_VPN = "COMMERCIAL_VPN"
8
+ ENTERPRISE_VPN = "ENTERPRISE_VPN"
9
+ MOBILE_PROXY = "MOBILE_PROXY"
10
+ BLOCKCHAIN_PROXY = "BLOCKCHAIN_PROXY"
11
+ RESIDENTIAL_PROXY = "RESIDENTIAL_PROXY"
12
+ PUBLIC_PROXY = "PUBLIC_PROXY"
13
+ DATACENTER_PROXY = "DATACENTER_PROXY"
14
+ TOR_NODE = "TOR_NODE"
15
+ PRIVATE_RELAY = "PRIVATE_RELAY"
16
+ BOTNET = "BOTNET"
17
+ SEARCH_ENGINE = "SEARCH_ENGINE"
18
+ AI_CRAWLER = "AI_CRAWLER"
19
+ SOCIAL_MEDIA = "SOCIAL_MEDIA"
20
+ UPTIME_MONITOR = "UPTIME_MONITOR"
21
+ LINK_PREVIEW = "LINK_PREVIEW"
22
+ SEO_CRAWLER = "SEO_CRAWLER"
23
+ WEB_ARCHIVER = "WEB_ARCHIVER"
24
+ WEBHOOK_PROVIDER = "WEBHOOK_PROVIDER"
25
+ PAYMENT_PROCESSOR = "PAYMENT_PROCESSOR"
26
+ INTERNET_SCANNER = "INTERNET_SCANNER"
27
+ end
28
+
29
+ # +type+ is one of ProxyType.
30
+ ProxyEvent = Model.define do
31
+ optional :ip, :string
32
+ optional :provider, :string
33
+ optional :type, :string
34
+ optional :country_code, :string
35
+ optional :asn, :integer
36
+ optional :timestamp, :unix_time
37
+ end
38
+
39
+ # +type+ is one of ProxyType, and +country_code+ and +asn+ describe
40
+ # +range_start+.
41
+ AnonymizerEvent = Model.define do
42
+ optional :range_start, :string
43
+ optional :range_end, :string
44
+ optional :provider, :string
45
+ optional :type, :string
46
+ optional :country_code, :string
47
+ optional :asn, :integer
48
+ optional :timestamp, :unix_time
49
+ end
50
+
51
+ # +length+ is in bytes.
52
+ TorrentFile = Model.define do
53
+ optional :path, :string
54
+ optional :length, :integer
55
+ end
56
+
57
+ # +source+ is where the peer was found, such as "DHT", "PEX", or "tracker".
58
+ TorrentPeer = Model.define do
59
+ optional :ip, :string
60
+ optional :port, :integer
61
+ optional :source, :string
62
+ optional :encrypted, :bool
63
+ end
64
+
65
+ # +info_hash+ is the hex SHA-1 info hash, and sizes are in bytes.
66
+ TorrentEvent = Model.define do
67
+ optional :info_hash, :string
68
+ optional :name, :string
69
+ optional :magnet_uri, :string
70
+ optional :total_size, :integer
71
+ optional :piece_length, :integer
72
+ optional :file_count, :integer
73
+ optional :files, [TorrentFile]
74
+ optional :peers, [TorrentPeer]
75
+ optional :timestamp, :unix_time
76
+ end
77
+
78
+ # A JA4T TCP fingerprint sighting. +type+ is one of ProxyType.
79
+ JA4TEvent = Model.define do
80
+ optional :ip, :string
81
+ optional :ja4t, :string
82
+ optional :provider, :string
83
+ optional :type, :string
84
+ optional :country_code, :string
85
+ optional :asn, :integer
86
+ optional :timestamp, :unix_time
87
+ end
88
+
89
+ # The Helios honeypot sensor that captured an event. +proxy_ip+ and
90
+ # +provider+ are the proxy exit address and service the traffic arrived
91
+ # through.
92
+ HeliosMeta = Model.define do
93
+ optional :proxy_ip, :string
94
+ optional :server, :string
95
+ optional :pool_id, :string
96
+ optional :provider, :string
97
+ end
98
+
99
+ # +http_method+ is the request method. The API calls it +method+, which
100
+ # would shadow Object#method.
101
+ HTTPRequestDetails = Model.define do
102
+ optional :http_method, :string, wire: "method"
103
+ optional :uri, :string
104
+ optional :version, :string
105
+ optional :headers, { string: :string }
106
+ end
107
+
108
+ # +domain+ is the host the proxy tunnel was opened to, and +raw+ holds the
109
+ # raw request bytes.
110
+ HeliosHTTPEvent = Model.define do
111
+ required :timestamp, :unix_time
112
+ required :tunnel_id, :integer
113
+ required :domain, :string
114
+ required :port, :integer
115
+ required :protocol, :string
116
+ required :meta, HeliosMeta
117
+ optional :details, HTTPRequestDetails
118
+ optional :raw, :bytes
119
+ end
120
+
121
+ # A TLS registry code point and its name. GREASE values are named
122
+ # "GREASE(0x....)".
123
+ TLSCodeName = Model.define do
124
+ optional :code, :integer
125
+ optional :name, :string
126
+ end
127
+
128
+ # +length+ is the extension body length in bytes.
129
+ TLSExtension = Model.define do
130
+ optional :code, :integer
131
+ optional :name, :string
132
+ optional :length, :integer
133
+ end
134
+
135
+ # Fields the sensor could not parse are nil.
136
+ TLSClientHello = Model.define do
137
+ optional :record_version, :string
138
+ optional :handshake_version, :string
139
+ optional :client_random, :string
140
+ optional :session_id, :string
141
+ optional :session_id_length, :integer
142
+ optional :cipher_suites, [TLSCodeName]
143
+ optional :compression_methods, [:integer]
144
+ optional :extensions, [TLSExtension]
145
+ optional :sni, :string
146
+ optional :alpn, [:string]
147
+ optional :supported_versions, [:string]
148
+ optional :supported_groups, [TLSCodeName]
149
+ optional :signature_algorithms, [TLSCodeName]
150
+ optional :ec_point_formats, [:string]
151
+ optional :key_share_groups, [:string]
152
+ optional :psk_key_exchange_modes, [:string]
153
+ optional :compress_certificate, [:string]
154
+ optional :has_grease, :bool
155
+ optional :application_settings, :bool
156
+ optional :delegated_credentials, :bool
157
+ optional :encrypt_then_mac, :bool
158
+ optional :extended_master_secret, :bool
159
+ optional :post_handshake_auth, :bool
160
+ optional :renegotiation_info, :bool
161
+ optional :signed_certificate_timestamps, :bool
162
+ optional :status_request, :bool
163
+ end
164
+
165
+ # A TLS ClientHello captured by a Helios honeypot. +details+ is nil when the
166
+ # sensor could not parse the handshake. +raw+ is always nil, because the
167
+ # sensors do not capture handshake bytes.
168
+ HeliosHTTPSEvent = Model.define do
169
+ required :timestamp, :unix_time
170
+ required :tunnel_id, :integer
171
+ required :domain, :string
172
+ required :port, :integer
173
+ required :protocol, :string
174
+ required :meta, HeliosMeta
175
+ optional :details, TLSClientHello
176
+ optional :raw, :string
177
+ end
178
+
179
+ # A destination a Helios honeypot tunnel was asked to reach. Despite the
180
+ # feed's name, it is not a DNS query: names resolved inside a tunneled DNS
181
+ # session are not visible.
182
+ HeliosDNSEvent = Model.define do
183
+ required :timestamp, :unix_time
184
+ required :tunnel_id, :integer
185
+ required :domain, :string
186
+ required :port, :integer
187
+ required :protocol, :string
188
+ required :meta, HeliosMeta
189
+ end
190
+
191
+ # An Android Debug Bridge shell command captured by a Helios honeypot. It
192
+ # has no timestamp because the sensor sends none. +sequential_id+ is the
193
+ # 1-based position of the command in its session. +payload_hash+ is the hex
194
+ # SHA-256 the sensor assigns to the session's payload, sent as +hash+: it is
195
+ # not a hash of +command+, and every command in a session shares it.
196
+ HeliosADBEvent = Model.define do
197
+ required :session, :string
198
+ required :sequential_id, :integer
199
+ required :command, :string
200
+ required :payload_hash, :string, wire: "hash"
201
+ end
202
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ module ExportKind
5
+ HOURLY = "hourly"
6
+ DAILY = "daily"
7
+ end
8
+
9
+ # New values may be added.
10
+ module ParquetType
11
+ STRING = "string"
12
+ INT64 = "int64"
13
+ UINT32 = "uint32"
14
+ UINT64 = "uint64"
15
+ BOOL = "bool"
16
+ BYTES = "bytes"
17
+ end
18
+
19
+ # +hour+ is nil for a daily export. +checksum+ is the hex SHA-256 of the
20
+ # file. +download_path+ includes the /api/v4 prefix.
21
+ Export = Model.define do
22
+ required :id, :string
23
+ required :kind, :string
24
+ required :date, :date
25
+ optional :hour, :integer
26
+ required :size_bytes, :integer
27
+ required :row_count, :integer
28
+ required :checksum, :string
29
+ required :created_at, :unix_time
30
+ required :download_path, :string
31
+ end
32
+
33
+ # +next_cursor+ is nil on the last page.
34
+ ExportPage = Model.define do
35
+ required :stream, :string
36
+ required :exports, [Export], wire: "feeds"
37
+ optional :next_cursor, :string
38
+ end
39
+
40
+ # +type+ is one of ParquetType.
41
+ ParquetColumn = Model.define do
42
+ required :name, :string
43
+ required :type, :string
44
+ end
45
+
46
+ ParquetSchema = Model.define do
47
+ required :fields, [ParquetColumn]
48
+ end
49
+
50
+ # +hour+ is nil for a daily export. +size+ is in bytes and +checksum+ is
51
+ # the hex SHA-256 of the file.
52
+ ExportMeta = Model.define do
53
+ required :id, :string
54
+ required :stream, :string
55
+ required :kind, :string
56
+ required :format, :string
57
+ required :date, :unix_time
58
+ optional :hour, :integer
59
+ required :rows, :integer
60
+ required :size, :integer
61
+ required :checksum, :string
62
+ required :created_at, :unix_time
63
+ required :schema, ParquetSchema
64
+ end
65
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # New values may be added.
5
+ module NetworkType
6
+ MOBILE = "MOBILE"
7
+ SATELLITE = "SATELLITE"
8
+ IN_FLIGHT_WIFI = "IN_FLIGHT_WIFI"
9
+ RESIDENTIAL = "RESIDENTIAL"
10
+ CORPORATE = "CORPORATE"
11
+ ACADEMIC = "ACADEMIC"
12
+ DATACENTER = "DATACENTER"
13
+ GOVERNMENT = "GOVERNMENT"
14
+ end
15
+
16
+ # Device operating systems. New values may be added.
17
+ module OS
18
+ ANDROID = "Android"
19
+ CHROME_OS = "Chrome OS"
20
+ FREEBSD = "FreeBSD"
21
+ IOS = "iOS"
22
+ LINUX = "Linux"
23
+ MAC_OS_X = "Mac OS X"
24
+ WINDOWS = "Windows"
25
+ end
26
+
27
+ # The network that announces an address. +asn+ is 0 if the address is not
28
+ # routed. +type+ is one of NetworkType. +org+, +domain+, +abuse_email+, and
29
+ # +abuse_phone+ are nil when unknown.
30
+ Network = Model.define do
31
+ required :asn, :integer
32
+ required :isp, :string
33
+ optional :org, :string
34
+ optional :domain, :string
35
+ required :type, :string
36
+ optional :abuse_email, :string
37
+ optional :abuse_phone, :string
38
+ end
39
+
40
+ # +country+ is an ISO 3166-1 alpha-2 code and +timezone+ an IANA time zone
41
+ # name.
42
+ Location = Model.define do
43
+ required :country, :string
44
+ required :state, :string
45
+ required :city, :string
46
+ required :timezone, :string
47
+ required :latitude, :float
48
+ required :longitude, :float
49
+ required :geo_hash, :string
50
+ end
51
+
52
+ # A proxy, VPN, or anonymizer service seen using an address. +name+ is the
53
+ # service tag, such as "BRIGHTDATA", and +type+ its category.
54
+ Provider = Model.define do
55
+ required :name, :string, wire: "provider"
56
+ required :type, :string
57
+ required :last_seen, :unix_time
58
+ end
59
+
60
+ # A device type seen behind an address. +os+ is one of OS.
61
+ Device = Model.define do
62
+ required :os, :string
63
+ required :version, :string
64
+ required :last_seen, :unix_time
65
+ end
66
+
67
+ # Synthient's assessment of an address. +risk_score+ runs from 0 to 100.
68
+ # +categories+ and +behavior+ are open sets of tags. An empty +providers+
69
+ # list does not mean the address is clean.
70
+ Intelligence = Model.define do
71
+ required :risk_score, :integer
72
+ required :categories, [:string]
73
+ required :behavior, [:string]
74
+ required :providers, [Provider]
75
+ required :devices, [Device]
76
+ end
77
+
78
+ # +network+, +location+, and +intelligence+ are nil when the API has no data
79
+ # for them.
80
+ IPInfo = Model.define do
81
+ required :ip, :ip
82
+ optional :network, Network
83
+ optional :location, Location
84
+ optional :intelligence, Intelligence
85
+ end
86
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Synthient
4
+ # The rate limit state reported in a response's RateLimit-* headers. +reset+
5
+ # is the number of seconds until the window resets.
6
+ RateLimit = Data.define(:limit, :remaining, :reset)
7
+
8
+ # Describes one API response. A client's +response_hook+ receives one for
9
+ # every response from the API, including retried attempts, export pages,
10
+ # stream handshakes, and responses that later fail to decode. Export
11
+ # downloads from storage are not reported.
12
+ #
13
+ # +headers+ is a frozen copy with lowercase names. +rate_limit+ is nil
14
+ # unless all three RateLimit-* headers are present and valid.
15
+ ResponseMetadata = Data.define(:status, :request_id, :rate_limit, :headers)
16
+ end