calmlens 0.20.0 → 0.21.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 +3 -1
- data/lib/calmlens/assets.rb +107 -98
- data/lib/calmlens/cdn.rb +197 -41
- data/lib/calmlens/error_codes.rb +80 -84
- data/lib/calmlens/errors.rb +2 -2
- data/lib/calmlens/events.rb +50 -1
- data/lib/calmlens/folders.rb +14 -9
- data/lib/calmlens/hash_lists.rb +29 -9
- data/lib/calmlens/internal.rb +9 -19
- data/lib/calmlens/lenses.rb +79 -6
- data/lib/calmlens/option_keys.rb +46 -25
- data/lib/calmlens/policies.rb +50 -18
- data/lib/calmlens/resource.rb +24 -6
- data/lib/calmlens/server_client.rb +8 -6
- data/lib/calmlens/session.rb +29 -12
- data/lib/calmlens/testing/asset_filters.rb +39 -0
- data/lib/calmlens/testing/classification.rb +8 -2
- data/lib/calmlens/testing/fake_api.rb +682 -368
- data/lib/calmlens/testing/mock_decision.rb +5 -4
- data/lib/calmlens/testing/rsql.rb +245 -0
- data/lib/calmlens/testing/test_input_detections.rb +61 -0
- data/lib/calmlens/testing/test_input_scenario.rb +6 -2
- data/lib/calmlens/testing/test_inputs.rb +31 -10
- data/lib/calmlens/testing.rb +36 -12
- data/lib/calmlens/transform_rules.rb +1 -1
- data/lib/calmlens/upload_mime_types.rb +9 -1
- data/lib/calmlens/upload_tokens.rb +3 -3
- data/lib/calmlens/version.rb +1 -1
- data/lib/calmlens/webhooks.rb +58 -13
- data/sig/calmlens.rbs +87 -13
- data/sig/error_codes.rbs +18 -20
- data/sig/qualifiers.rbs +2 -0
- data/sig/types.rbs +326 -187
- metadata +5 -2
data/lib/calmlens/cdn.rb
CHANGED
|
@@ -1,33 +1,129 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module CalmLens
|
|
4
|
-
#
|
|
4
|
+
# Asset URLs, signed asset URLs, asset info and authenticated content delivery. Reach it through
|
|
5
|
+
# {ServerClient#cdn}.
|
|
6
|
+
#
|
|
7
|
+
# Every asset has one URL, `{cdn_base_url}/assets/{project_id}/{asset}`, where `{asset}` is the asset
|
|
8
|
+
# ID or `ext:` and the URL-encoded external ID. Each resource is a path beneath it, such as
|
|
9
|
+
# `/rendered` or `/preview/@thumbnail,width=320`. A signed asset URL ({#sign_asset_url}) replaces
|
|
10
|
+
# `{asset}` with a grant, and the same resources are paths beneath it.
|
|
5
11
|
class CDN < Resource
|
|
6
12
|
# The prefix of this resource's operations in Internal::OPTION_KEYS.
|
|
7
13
|
OPERATIONS = 'cdn'
|
|
8
14
|
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
# Where each resource is served beneath an asset URL. The adaptive stream is its HLS master
|
|
16
|
+
# playlist; its media playlists and segments are the other files beneath `hls/`.
|
|
17
|
+
RESOURCE_PATHS = {
|
|
18
|
+
'original' => 'original', 'rendered' => 'rendered', 'preview' => 'preview',
|
|
19
|
+
'hls' => 'hls/index.m3u8', 'embed' => 'embed', 'placeholder' => 'placeholder', 'info' => 'info'
|
|
20
|
+
}.freeze
|
|
21
|
+
private_constant :RESOURCE_PATHS
|
|
11
22
|
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
#
|
|
23
|
+
# Every CDN resource beneath an asset URL, as {#get_asset_url} and {#sign_asset_url} accept them.
|
|
24
|
+
RESOURCES = RESOURCE_PATHS.keys.freeze
|
|
25
|
+
|
|
26
|
+
# A signed asset URL's lifetime in seconds when `expires_in` is omitted: 15 minutes.
|
|
27
|
+
DEFAULT_SIGNED_URL_EXPIRES_IN = 15 * 60
|
|
28
|
+
# The longest signed asset URL lifetime the API accepts, in seconds: 24 hours.
|
|
29
|
+
MAX_SIGNED_URL_EXPIRES_IN = 24 * 60 * 60
|
|
30
|
+
|
|
31
|
+
# The start of the asset segment of a URL that names the asset by its external ID.
|
|
32
|
+
EXTERNAL_ID_PREFIX = 'ext:'
|
|
33
|
+
|
|
34
|
+
# The ISO 3166-1 alpha-2 countries a viewer's `region` can be. EU is not a viewer country.
|
|
35
|
+
VIEWER_COUNTRIES = %w[
|
|
36
|
+
AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY
|
|
37
|
+
BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK
|
|
38
|
+
FM FO FR GA GB GD GE GF GG GH GI GL GM GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE IL IM IN IO IQ IR
|
|
39
|
+
IS IT JE JM JO JP KE KG KH KI KM KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY MA MC MD ME MF MG MH MK
|
|
40
|
+
ML MM MN MO MP MQ MR MS MT MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU NZ OM PA PE PF PG PH PK PL PM
|
|
41
|
+
PN PR PS PT PW PY QA RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM SN SO SR SS ST SV SX SY SZ TC TD TF
|
|
42
|
+
TG TH TJ TK TL TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE VG VI VN VU WF WS YE YT ZA ZM ZW
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
# Builds an asset's CDN URL without a request: the asset URL, which every resource is a path
|
|
46
|
+
# beneath, or a resource URL when `resource` is given, with its lens or transform and the viewer
|
|
47
|
+
# claims. Use it for public assets; private ones need {#sign_asset_url} (or the API key, through
|
|
48
|
+
# {#serve}).
|
|
49
|
+
# @option options [String] :asset_id the asset ID; give it or `external_id`, not both
|
|
50
|
+
# @option options [String] :external_id the external ID the asset was uploaded with, written as
|
|
51
|
+
# `ext:` and the URL-encoded ID
|
|
52
|
+
# @option options [String] :project_id the asset's project; the client's by default
|
|
53
|
+
# @option options [String] :resource `rendered` (the displayable file: an image in the best format the
|
|
54
|
+
# client accepts, a PDF, or browser-safe video or audio), `original` (the exact upload), `preview` (a
|
|
55
|
+
# still image for the asset), `hls` (the HLS master playlist of audio or video, at `hls/index.m3u8`,
|
|
56
|
+
# once `content.hls` is set), `embed` (an HTML page that shows the asset, a viewer for its type, for an
|
|
57
|
+
# iframe), `placeholder` (a small blurred PNG that the CDN serves publicly for every asset whatever its
|
|
58
|
+
# visibility or moderation status, without age confirmation) or `info` (the asset's display info, as
|
|
59
|
+
# {#get_asset_info} reads it). Without it, the URL is the asset URL itself. Lenses and transforms
|
|
60
|
+
# apply to `preview`, and to `rendered` when it is an image; the CDN refuses them on rendered PDFs,
|
|
61
|
+
# video and audio with `transform/invalid`, and the SDK refuses them on the other resources and
|
|
62
|
+
# without a resource.
|
|
18
63
|
# @option options [String] :lens a lens identifier
|
|
19
64
|
# @option options [String, Hash] :transform options applied in one pass: a selector such as
|
|
20
65
|
# `width=200,fit=cover`, or structured options such as `{width: 200, fit: 'cover'}`. With a lens,
|
|
21
66
|
# they replace the lens's options of the same name
|
|
22
|
-
# @option options [Hash] :viewer_claims
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
67
|
+
# @option options [Hash] :viewer_claims `{region:, age_confirmed:}`, sent as a JSON `viewerClaims` query
|
|
68
|
+
# parameter, leaving out the defaults (no region, age not confirmed). `region` is an ISO 3166-1
|
|
69
|
+
# alpha-2 country. The CDN serves an asset whose moderation status is `age-restricted` only with
|
|
70
|
+
# `age_confirmed: true`, even with a signed URL or the API key. Set it only where your app has
|
|
71
|
+
# already decided the viewer may see age-restricted content. Your platform confirms the viewer's
|
|
72
|
+
# age; CalmLens does not verify this claim, and anyone can add it to a URL.
|
|
28
73
|
# @return [String]
|
|
29
74
|
def get_asset_url(options)
|
|
30
|
-
call(options, __method__) { |api, _request|
|
|
75
|
+
call(options, __method__) { |api, _request| asset_url(api) }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Signs a private asset's URL on your server, so browsers and apps can load it without an API key
|
|
79
|
+
# until it expires. Every resource is a path beneath it, as beneath a public asset URL; `original`
|
|
80
|
+
# only when signed with `original: true`. With `resource`, returns that resource's URL beneath the
|
|
81
|
+
# grant; lenses, transforms and viewer claims are appended as {#get_asset_url} appends them and
|
|
82
|
+
# aren't part of the signature.
|
|
83
|
+
# @option options [String] :asset_id give it or `external_id`, not both
|
|
84
|
+
# @option options [String] :external_id
|
|
85
|
+
# @option options [String] :project_id the client's by default
|
|
86
|
+
# @option options [Integer] :expires_in whole seconds from 1 to 86400; 900 by default
|
|
87
|
+
# @option options [Boolean] :original whether the URL also serves the exact upload (`original`); false by default
|
|
88
|
+
# @option options [String] :resource see {#get_asset_url}
|
|
89
|
+
# @option options [String] :lens
|
|
90
|
+
# @option options [String, Hash] :transform
|
|
91
|
+
# @option options [Hash] :viewer_claims
|
|
92
|
+
# @return [Hash] `{'url', 'expiresAt'}`, `expiresAt` in milliseconds since the Unix epoch
|
|
93
|
+
def sign_asset_url(options)
|
|
94
|
+
call(options, __method__) do |api, request|
|
|
95
|
+
check_signing(api)
|
|
96
|
+
project = Internal.segment(api.fetch('projectId', @session.project_id))
|
|
97
|
+
body = api.slice('assetId', 'externalId')
|
|
98
|
+
body['expiresIn'] = api.fetch('expiresIn', DEFAULT_SIGNED_URL_EXPIRES_IN)
|
|
99
|
+
body['original'] = api.fetch('original', false)
|
|
100
|
+
signed = request('POST', "/projects/#{project}/signed-asset-urls", body: body, request_options: request)
|
|
101
|
+
unless signed.is_a?(Hash) && signed['url'].is_a?(String) && !signed['url'].empty? && signed['expiresAt'].is_a?(Integer)
|
|
102
|
+
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'The API returned no signed asset URL')
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
{ 'url' => append_location(signed['url'], api), 'expiresAt' => signed['expiresAt'] }
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Reads the display info of an asset URL, public or signed: `{url}/info`, with the asset's type,
|
|
110
|
+
# size, rendered file, preview and stream, and whether the viewer its claims describe can see it
|
|
111
|
+
# now (`availability`: `available`, `age-restricted` or `unavailable`). It sends no API key, as a
|
|
112
|
+
# browser would, so a private asset's info needs a signed URL.
|
|
113
|
+
# @param url [String] an asset URL from {#get_asset_url} or {#sign_asset_url}, without a resource
|
|
114
|
+
# @param viewer_claims [Hash, nil] the viewer's claims, as for {#get_asset_url}; the URL's own
|
|
115
|
+
# `viewerClaims` when omitted
|
|
116
|
+
# @return [Hash] the asset info
|
|
117
|
+
# @raise [APIError] with the CDN's code, such as `signed-url/expired` or `asset/not-available`
|
|
118
|
+
def get_asset_info(url, viewer_claims: nil)
|
|
119
|
+
base, claims = split_asset_url(url)
|
|
120
|
+
claims = Internal.normalize(viewer_claims) unless viewer_claims.nil?
|
|
121
|
+
info = @session.cdn_json("#{base}/info#{viewer_claims_query(claims)}")
|
|
122
|
+
return info if asset_info?(info)
|
|
123
|
+
|
|
124
|
+
raise InvalidResponseError.new(SdkErrorCodes::INVALID_RESPONSE, 'The CDN returned no asset info')
|
|
125
|
+
rescue TypeError, ArgumentError, JSON::GeneratorError, EncodingError, URI::Error => e
|
|
126
|
+
raise InvalidArgumentError.new(SdkErrorCodes::INVALID_ARGUMENT, e.message), cause: e
|
|
31
127
|
end
|
|
32
128
|
|
|
33
129
|
# Fetches content from the CDN with the client's API key.
|
|
@@ -37,14 +133,19 @@ module CalmLens
|
|
|
37
133
|
# block, returns the {Response} with its body read into memory. Error statuses are returned,
|
|
38
134
|
# not raised; check {Response#status}.
|
|
39
135
|
#
|
|
40
|
-
# @option options [String] :
|
|
136
|
+
# @option options [String] :asset_id see {#get_asset_url}
|
|
137
|
+
# @option options [String] :external_id
|
|
138
|
+
# @option options [String] :project_id
|
|
139
|
+
# @option options [String] :resource `rendered` by default
|
|
140
|
+
# @option options [String] :lens
|
|
141
|
+
# @option options [String, Hash] :transform
|
|
41
142
|
# @option options [Hash] :viewer_claims see {#get_asset_url}
|
|
42
143
|
# @option options [Hash] :init `{method: 'GET' | 'HEAD', headers: {...}}`
|
|
43
144
|
# @yieldparam response [Response]
|
|
44
145
|
# @return [Response, Object]
|
|
45
146
|
# @example Save content to a file
|
|
46
147
|
# File.open('photo.jpg', 'wb') do |file|
|
|
47
|
-
# client.cdn.serve(
|
|
148
|
+
# client.cdn.serve(asset_id: id) do |response|
|
|
48
149
|
# raise "CDN returned #{response.status}" unless response.success?
|
|
49
150
|
# response.each_chunk { |chunk| file.write(chunk) }
|
|
50
151
|
# end
|
|
@@ -57,42 +158,97 @@ module CalmLens
|
|
|
57
158
|
verb = init.fetch('method', 'GET').to_s.upcase
|
|
58
159
|
raise Internal.invalid('init.method must be GET or HEAD') unless %w[GET HEAD].include?(verb)
|
|
59
160
|
|
|
60
|
-
[verb,
|
|
161
|
+
[verb, asset_url({ 'resource' => 'rendered' }.merge(api)), Internal.headers(init.fetch('headers', {})), request_options]
|
|
61
162
|
end
|
|
62
163
|
@session.cdn(method, url, headers, request, &)
|
|
63
164
|
end
|
|
64
165
|
|
|
65
166
|
private
|
|
66
167
|
|
|
67
|
-
def
|
|
68
|
-
|
|
69
|
-
|
|
168
|
+
def asset_url(api)
|
|
169
|
+
project = Internal.segment(api.fetch('projectId', @session.project_id))
|
|
170
|
+
append_location("#{@session.cdn_base_url}/assets/#{project}/#{asset_segment(api)}", api)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# The asset segment of a URL: the asset ID, or `ext:` and the URL-encoded external ID.
|
|
174
|
+
def asset_segment(api)
|
|
175
|
+
raise Internal.invalid('Pass either asset_id or external_id') if api.key?('assetId') == api.key?('externalId')
|
|
176
|
+
return Internal.segment(api['assetId']) if api.key?('assetId')
|
|
70
177
|
|
|
71
|
-
|
|
72
|
-
raise Internal.invalid('
|
|
73
|
-
raise Internal.invalid('A placeholder takes no lens or transform') if resource == 'placeholder' && transformed
|
|
178
|
+
external = api['externalId']
|
|
179
|
+
raise Internal.invalid('external_id must be a non-empty String') unless external.is_a?(String) && !external.empty?
|
|
74
180
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
transform = api['transform']
|
|
78
|
-
selector = [("@#{Internal.segment(api['lens'])}" if api['lens']),
|
|
79
|
-
(Transforms.path(transform) unless transform.nil? || transform == '')].compact.join(',')
|
|
80
|
-
url += "/#{selector}" unless selector.empty?
|
|
181
|
+
EXTERNAL_ID_PREFIX + Internal.component(external)
|
|
182
|
+
end
|
|
81
183
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
# Appends a resource, its `@lens` and transform selector, and the viewer claims to an asset URL
|
|
185
|
+
# (public or signed). Without a resource the URL stays the asset URL, which takes no selector.
|
|
186
|
+
def append_location(url, api)
|
|
187
|
+
resource = api['resource']
|
|
188
|
+
selected = !api['lens'].nil? || !api['transform'].nil?
|
|
189
|
+
if resource.nil?
|
|
190
|
+
raise Internal.invalid('A lens or transform needs a resource, such as rendered') if selected
|
|
191
|
+
else
|
|
192
|
+
raise Internal.invalid("resource must be one of #{RESOURCES.join(', ')}") unless RESOURCE_PATHS.key?(resource)
|
|
193
|
+
if Internal::UNSELECTABLE_CDN_RESOURCES.include?(resource) && selected
|
|
194
|
+
raise Internal.invalid("The #{resource} resource takes no lens or transform")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
url = "#{url}/#{RESOURCE_PATHS.fetch(resource)}"
|
|
198
|
+
transform = api['transform']
|
|
199
|
+
selector = [("@#{Internal.segment(api['lens'])}" if api['lens']),
|
|
200
|
+
(Transforms.path(transform) unless transform.nil? || transform == '')].compact.join(',')
|
|
201
|
+
url += "/#{selector}" unless selector.empty?
|
|
202
|
+
end
|
|
203
|
+
"#{url}#{viewer_claims_query(api['viewerClaims'])}"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Checks sign_asset_url's own options as the API does, then the location, before anything is sent.
|
|
207
|
+
def check_signing(api)
|
|
208
|
+
expires = api.fetch('expiresIn', DEFAULT_SIGNED_URL_EXPIRES_IN)
|
|
209
|
+
unless expires.is_a?(Integer) && expires.between?(1, MAX_SIGNED_URL_EXPIRES_IN)
|
|
210
|
+
raise Internal.invalid("expires_in must be a whole number of seconds from 1 to #{MAX_SIGNED_URL_EXPIRES_IN}")
|
|
85
211
|
end
|
|
212
|
+
raise Internal.invalid('original must be true or false') unless [true, false].include?(api.fetch('original', false))
|
|
213
|
+
|
|
214
|
+
asset_segment(api)
|
|
215
|
+
append_location('', api)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# The `?viewerClaims=` query of a URL with these claims: the region, and `ageConfirmed` only when
|
|
219
|
+
# true. The defaults are left out, so default URLs stay cacheable.
|
|
220
|
+
def viewer_claims_query(claims)
|
|
221
|
+
return '' if claims.nil?
|
|
86
222
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
223
|
+
unless claims.is_a?(Hash) && (claims.keys - %w[region ageConfirmed]).empty? &&
|
|
224
|
+
(!claims.key?('region') || VIEWER_COUNTRIES.include?(claims['region'])) &&
|
|
225
|
+
(!claims.key?('ageConfirmed') || [true, false].include?(claims['ageConfirmed']))
|
|
226
|
+
raise Internal.invalid('Invalid viewer_claims: region is an ISO 3166-1 alpha-2 country and age_confirmed a boolean')
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
sent = {}
|
|
230
|
+
sent['region'] = claims['region'] if claims.key?('region')
|
|
231
|
+
sent['ageConfirmed'] = true if claims['ageConfirmed'] == true
|
|
232
|
+
sent.empty? ? '' : "?viewerClaims=#{URI.encode_www_form_component(JSON.generate(sent))}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# An asset URL without its query, and the viewer claims it carried, if any.
|
|
236
|
+
def split_asset_url(url)
|
|
237
|
+
raise Internal.invalid('url must be an absolute asset URL') unless url.is_a?(String) && url.match?(%r{\Ahttps?://[^/?#]+/}i)
|
|
238
|
+
|
|
239
|
+
path, query = url.split('#', 2).first.split('?', 2)
|
|
240
|
+
raw = URI.decode_www_form(query.to_s).to_h['viewerClaims']
|
|
241
|
+
claims = begin
|
|
242
|
+
raw && JSON.parse(raw)
|
|
243
|
+
rescue JSON::ParserError
|
|
244
|
+
raise Internal.invalid("The asset URL's viewerClaims are not JSON")
|
|
245
|
+
end
|
|
246
|
+
[path.sub(%r{/+\z}, ''), claims]
|
|
90
247
|
end
|
|
91
248
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
known.merge(claims.except(*known.keys))
|
|
249
|
+
def asset_info?(info)
|
|
250
|
+
info.is_a?(Hash) && info['id'].is_a?(String) && info['projectId'].is_a?(String) && info['type'].is_a?(String) &&
|
|
251
|
+
info['content'].is_a?(Hash) && %w[available age-restricted unavailable].include?(info['availability'])
|
|
96
252
|
end
|
|
97
253
|
end
|
|
98
254
|
end
|
data/lib/calmlens/error_codes.rb
CHANGED
|
@@ -5,63 +5,63 @@
|
|
|
5
5
|
module CalmLens
|
|
6
6
|
# Error codes the CalmLens API returns, for comparing with {Error#code}.
|
|
7
7
|
module ErrorCodes
|
|
8
|
-
INVALID_VIEWER_CLAIMS = 'viewer/invalid
|
|
8
|
+
INVALID_VIEWER_CLAIMS = 'viewer-claims/invalid'
|
|
9
9
|
AGE_CONFIRMATION_REQUIRED = 'viewer/age-confirmation-required'
|
|
10
|
-
INVALID_VIEWER_REGION = 'viewer/invalid
|
|
10
|
+
INVALID_VIEWER_REGION = 'viewer-region/invalid'
|
|
11
11
|
VIEWER_REGION_REQUIRED = 'viewer/region-required'
|
|
12
12
|
REGIONAL_EVIDENCE_REQUIRED = 'asset/regional-evidence-required'
|
|
13
13
|
SIGNED_URL_EXPIRED = 'signed-url/expired'
|
|
14
14
|
SIGNED_URL_INVALID = 'signed-url/invalid'
|
|
15
|
+
SIGNED_URL_ORIGINAL_NOT_GRANTED = 'signed-url/original-not-granted'
|
|
16
|
+
INVALID_REQUEST = 'validation/invalid-request'
|
|
15
17
|
INVALID_TYPE = 'validation/invalid-type'
|
|
16
18
|
TOO_SMALL = 'validation/too-small'
|
|
17
19
|
TOO_BIG = 'validation/too-big'
|
|
18
20
|
INVALID_FORMAT = 'validation/invalid-format'
|
|
19
21
|
INVALID_VALUE = 'validation/invalid-value'
|
|
20
22
|
UNKNOWN_FIELD = 'validation/unknown-field'
|
|
21
|
-
UNSUPPORTED_CONTENT_TYPE = '
|
|
22
|
-
INVALID_FILTER = '
|
|
23
|
-
|
|
24
|
-
ROUTE_NOT_FOUND = '
|
|
25
|
-
INTERNAL_ERROR = '
|
|
23
|
+
UNSUPPORTED_CONTENT_TYPE = 'request/unsupported-content-type'
|
|
24
|
+
INVALID_FILTER = 'filter/invalid'
|
|
25
|
+
UNSUPPORTED_ORDERING_FIELD = 'ordering/unsupported-field'
|
|
26
|
+
ROUTE_NOT_FOUND = 'request/route-not-found'
|
|
27
|
+
INTERNAL_ERROR = 'server/internal-error'
|
|
26
28
|
UNAUTHENTICATED = 'auth/unauthenticated'
|
|
27
|
-
TOKEN_EXPIRED = '
|
|
28
|
-
INVALID_TOKEN = '
|
|
29
|
+
TOKEN_EXPIRED = 'session-token/expired'
|
|
30
|
+
INVALID_TOKEN = 'session-token/invalid'
|
|
29
31
|
SEARCH_UNAVAILABLE = 'search/unavailable'
|
|
30
|
-
INVALID_TRANSFORM = '
|
|
31
|
-
INVALID_TRANSFORM_OPTIONS = '
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
API_KEY_PROJECT_MISMATCH = 'auth/project-mismatch'
|
|
39
|
-
API_KEY_NOT_ACCEPTED = 'auth/api-key-not-accepted'
|
|
32
|
+
INVALID_TRANSFORM = 'transform/invalid'
|
|
33
|
+
INVALID_TRANSFORM_OPTIONS = 'transform-options/invalid'
|
|
34
|
+
REMOTE_URL_TOO_MANY_REDIRECTS = 'remote-url/too-many-redirects'
|
|
35
|
+
INVALID_CURSOR = 'cursor/invalid'
|
|
36
|
+
MISSING_API_KEY = 'api-key/missing'
|
|
37
|
+
INVALID_API_KEY = 'api-key/invalid'
|
|
38
|
+
API_KEY_PROJECT_MISMATCH = 'api-key/project-mismatch'
|
|
39
|
+
API_KEY_NOT_ACCEPTED = 'api-key/not-accepted'
|
|
40
40
|
PERMISSION_DENIED = 'auth/permission-denied'
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
INVALID_UPLOAD_MIME_TYPE = 'upload/invalid-mime-type'
|
|
50
|
-
INVALID_UPLOAD_PAYLOAD = 'upload/invalid-payload'
|
|
41
|
+
UPLOAD_TOO_LARGE = 'upload/too-large'
|
|
42
|
+
DOCUMENT_TOO_MANY_ITEMS = 'document/too-many-items'
|
|
43
|
+
INVALID_IMAGE = 'image/invalid'
|
|
44
|
+
IMAGE_TOO_LARGE = 'image/too-large'
|
|
45
|
+
FREE_UPLOADS_EXHAUSTED = 'usage/free-uploads-exhausted'
|
|
46
|
+
UPLOADS_PAUSED = 'usage/uploads-paused'
|
|
47
|
+
UNSUPPORTED_UPLOAD_TYPE = 'upload/unsupported-type'
|
|
48
|
+
INVALID_UPLOAD_PAYLOAD = 'upload-payload/invalid'
|
|
51
49
|
USE_UPLOAD_PART = 'upload/use-upload-part'
|
|
52
50
|
UPLOAD_NOT_OPEN = 'upload/not-open'
|
|
53
|
-
INVALID_UPLOAD_PART_NUMBER = 'upload/invalid-
|
|
51
|
+
INVALID_UPLOAD_PART_NUMBER = 'upload-part/invalid-number'
|
|
54
52
|
UPLOAD_MISSING = 'upload/missing'
|
|
55
53
|
UPLOAD_SIZE_MISMATCH = 'upload/size-mismatch'
|
|
56
54
|
INVALID_IDEMPOTENCY_KEY = 'idempotency/invalid-key'
|
|
57
55
|
IDEMPOTENCY_KEY_REUSED = 'idempotency/key-reused'
|
|
58
56
|
IDEMPOTENT_REQUEST_IN_PROGRESS = 'idempotency/request-in-progress'
|
|
57
|
+
NO_RENDERED_VERSION = 'asset/no-rendered-version'
|
|
59
58
|
ASSET_NOT_FOUND = 'asset/not-found'
|
|
60
59
|
PROJECT_NOT_FOUND = 'project/not-found'
|
|
60
|
+
ASSET_CHILD_NOT_DELETABLE = 'asset/child-not-deletable'
|
|
61
61
|
ASSET_REVIEW_CHANGED = 'asset/review-changed'
|
|
62
62
|
ASSET_REVIEW_NOTE_REQUIRED = 'asset/review-note-required'
|
|
63
63
|
ASSET_UPDATE_FAILED = 'asset/update-failed'
|
|
64
|
-
|
|
64
|
+
ASSET_HELD = 'asset/held'
|
|
65
65
|
ASSET_PROCESSING = 'asset/processing'
|
|
66
66
|
ASSET_DECISION_NOT_ALLOWED = 'asset/decision-not-allowed'
|
|
67
67
|
ASSET_DECISION_UNCHANGED = 'asset/decision-unchanged'
|
|
@@ -71,18 +71,19 @@ module CalmLens
|
|
|
71
71
|
ASSET_NOT_DECIDED = 'asset/not-decided'
|
|
72
72
|
ASSET_POLICY_CHANGED = 'asset/policy-changed'
|
|
73
73
|
ASSET_REVIEWED = 'asset/reviewed'
|
|
74
|
-
SUPPORT_TICKET_NOT_FOUND = 'support/
|
|
74
|
+
SUPPORT_TICKET_NOT_FOUND = 'support-ticket/not-found'
|
|
75
75
|
DUPLICATE_EXTERNAL_ID = 'asset/duplicate-external-id'
|
|
76
76
|
FOLDER_NOT_FOUND = 'folder/not-found'
|
|
77
77
|
DUPLICATE_FOLDER_NAME = 'folder/duplicate-name'
|
|
78
78
|
FOLDER_NOT_EMPTY = 'folder/not-empty'
|
|
79
79
|
FOLDER_PARENT_CYCLE = 'folder/parent-cycle'
|
|
80
|
-
|
|
80
|
+
LENS_NOT_FOUND = 'lens/not-found'
|
|
81
|
+
LENS_IDENTIFIER_TAKEN = 'lens/identifier-taken'
|
|
81
82
|
LENS_IDENTIFIER_RESERVED = 'lens/identifier-reserved'
|
|
82
83
|
HASH_LIST_NOT_FOUND = 'hash-list/not-found'
|
|
83
|
-
HASH_LIST_ENTRY_NOT_FOUND = 'hash-list/
|
|
84
|
+
HASH_LIST_ENTRY_NOT_FOUND = 'hash-list-entry/not-found'
|
|
84
85
|
HASH_LIST_LIMIT_REACHED = 'hash-list/limit-reached'
|
|
85
|
-
HASH_LIST_ENTRY_LIMIT_REACHED = 'hash-list/
|
|
86
|
+
HASH_LIST_ENTRY_LIMIT_REACHED = 'hash-list-entry/limit-reached'
|
|
86
87
|
HASH_LIST_ASSET_NOT_HASHED = 'hash-list/asset-not-hashed'
|
|
87
88
|
POLICY_NOT_FOUND = 'policy/not-found'
|
|
88
89
|
POLICY_REVISION_CONFLICT = 'policy/revision-conflict'
|
|
@@ -96,18 +97,16 @@ module CalmLens
|
|
|
96
97
|
UPLOAD_PRESET_NOT_FOUND = 'upload-preset/not-found'
|
|
97
98
|
UPLOAD_PRESET_DISABLED = 'upload-preset/disabled'
|
|
98
99
|
UPLOAD_PRESET_TOKEN_REQUIRED = 'upload-preset/token-required'
|
|
99
|
-
REMOTE_URL_NOT_ALLOWED = '
|
|
100
|
-
|
|
101
|
-
UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED = 'upload-preset/content-type-not-allowed'
|
|
102
|
-
UPLOAD_PRESET_TOO_LARGE = 'upload-preset/too-large'
|
|
100
|
+
REMOTE_URL_NOT_ALLOWED = 'remote-url/not-allowed'
|
|
101
|
+
WEBSITE_RATE_LIMIT_REACHED = 'upload/website-rate-limit-reached'
|
|
103
102
|
UPLOAD_PRESET_DAILY_LIMIT_REACHED = 'upload-preset/daily-limit-reached'
|
|
104
103
|
UPLOAD_PRESET_ORIGIN_NOT_ALLOWED = 'upload-preset/origin-not-allowed'
|
|
105
104
|
UPLOAD_PRESET_FIELD_NOT_ALLOWED = 'upload-preset/field-not-allowed'
|
|
106
105
|
UPLOAD_TOKEN_INVALID = 'upload-token/invalid'
|
|
107
106
|
UPLOAD_TOKEN_EXPIRED = 'upload-token/expired'
|
|
108
107
|
UPLOAD_TOKEN_USED = 'upload-token/used'
|
|
109
|
-
INVALID_REMOTE_URL = '
|
|
110
|
-
REMOTE_URL_UNAVAILABLE = '
|
|
108
|
+
INVALID_REMOTE_URL = 'remote-url/invalid'
|
|
109
|
+
REMOTE_URL_UNAVAILABLE = 'remote-url/unavailable'
|
|
111
110
|
APPEAL_NOT_FOUND = 'appeal/not-found'
|
|
112
111
|
APPEAL_JUDGMENT_NOT_FOUND = 'appeal/judgment-not-found'
|
|
113
112
|
APPEAL_ASSET_NOT_FOUND = 'appeal/asset-not-found'
|
|
@@ -115,68 +114,67 @@ module CalmLens
|
|
|
115
114
|
APPEAL_ALREADY_REVIEWED = 'appeal/already-reviewed'
|
|
116
115
|
APPEAL_DECISION_CHANGED = 'appeal/decision-changed'
|
|
117
116
|
APPEAL_INVALID_ACTION = 'appeal/invalid-action'
|
|
118
|
-
APPEAL_SAFETY_HELD = 'appeal/safety-held'
|
|
119
117
|
APPEAL_CONTENT_UNAVAILABLE = 'appeal/content-unavailable'
|
|
120
118
|
|
|
121
119
|
# The HTTP status the API returns with each code.
|
|
122
120
|
STATUSES = {
|
|
123
|
-
'viewer/invalid
|
|
121
|
+
'viewer-claims/invalid' => 400,
|
|
124
122
|
'viewer/age-confirmation-required' => 403,
|
|
125
|
-
'viewer/invalid
|
|
123
|
+
'viewer-region/invalid' => 400,
|
|
126
124
|
'viewer/region-required' => 403,
|
|
127
125
|
'asset/regional-evidence-required' => 403,
|
|
128
126
|
'signed-url/expired' => 403,
|
|
129
127
|
'signed-url/invalid' => 403,
|
|
128
|
+
'signed-url/original-not-granted' => 403,
|
|
129
|
+
'validation/invalid-request' => 400,
|
|
130
130
|
'validation/invalid-type' => 400,
|
|
131
131
|
'validation/too-small' => 400,
|
|
132
132
|
'validation/too-big' => 400,
|
|
133
133
|
'validation/invalid-format' => 400,
|
|
134
134
|
'validation/invalid-value' => 400,
|
|
135
135
|
'validation/unknown-field' => 400,
|
|
136
|
-
'
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
'
|
|
136
|
+
'request/unsupported-content-type' => 415,
|
|
137
|
+
'filter/invalid' => 400,
|
|
138
|
+
'ordering/unsupported-field' => 400,
|
|
139
|
+
'request/route-not-found' => 404,
|
|
140
|
+
'server/internal-error' => 500,
|
|
141
141
|
'auth/unauthenticated' => 401,
|
|
142
|
-
'
|
|
143
|
-
'
|
|
142
|
+
'session-token/expired' => 401,
|
|
143
|
+
'session-token/invalid' => 401,
|
|
144
144
|
'search/unavailable' => 503,
|
|
145
|
-
'
|
|
146
|
-
'
|
|
147
|
-
'
|
|
148
|
-
'
|
|
149
|
-
'
|
|
150
|
-
'
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'auth/project-mismatch' => 403,
|
|
154
|
-
'auth/api-key-not-accepted' => 403,
|
|
145
|
+
'transform/invalid' => 400,
|
|
146
|
+
'transform-options/invalid' => 400,
|
|
147
|
+
'remote-url/too-many-redirects' => 400,
|
|
148
|
+
'cursor/invalid' => 400,
|
|
149
|
+
'api-key/missing' => 401,
|
|
150
|
+
'api-key/invalid' => 401,
|
|
151
|
+
'api-key/project-mismatch' => 403,
|
|
152
|
+
'api-key/not-accepted' => 403,
|
|
155
153
|
'auth/permission-denied' => 403,
|
|
156
|
-
'upload/
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'image/
|
|
160
|
-
'
|
|
161
|
-
'
|
|
162
|
-
'upload/
|
|
163
|
-
'upload/
|
|
164
|
-
'upload/invalid-mime-type' => 400,
|
|
165
|
-
'upload/invalid-payload' => 400,
|
|
154
|
+
'upload/too-large' => 413,
|
|
155
|
+
'document/too-many-items' => 422,
|
|
156
|
+
'image/invalid' => 422,
|
|
157
|
+
'image/too-large' => 422,
|
|
158
|
+
'usage/free-uploads-exhausted' => 403,
|
|
159
|
+
'usage/uploads-paused' => 402,
|
|
160
|
+
'upload/unsupported-type' => 415,
|
|
161
|
+
'upload-payload/invalid' => 400,
|
|
166
162
|
'upload/use-upload-part' => 409,
|
|
167
163
|
'upload/not-open' => 409,
|
|
168
|
-
'upload/invalid-
|
|
164
|
+
'upload-part/invalid-number' => 409,
|
|
169
165
|
'upload/missing' => 409,
|
|
170
166
|
'upload/size-mismatch' => 400,
|
|
171
167
|
'idempotency/invalid-key' => 400,
|
|
172
168
|
'idempotency/key-reused' => 409,
|
|
173
169
|
'idempotency/request-in-progress' => 409,
|
|
170
|
+
'asset/no-rendered-version' => 404,
|
|
174
171
|
'asset/not-found' => 404,
|
|
175
172
|
'project/not-found' => 404,
|
|
173
|
+
'asset/child-not-deletable' => 409,
|
|
176
174
|
'asset/review-changed' => 409,
|
|
177
175
|
'asset/review-note-required' => 400,
|
|
178
176
|
'asset/update-failed' => 500,
|
|
179
|
-
'asset/
|
|
177
|
+
'asset/held' => 423,
|
|
180
178
|
'asset/processing' => 409,
|
|
181
179
|
'asset/decision-not-allowed' => 409,
|
|
182
180
|
'asset/decision-unchanged' => 409,
|
|
@@ -186,18 +184,19 @@ module CalmLens
|
|
|
186
184
|
'asset/not-decided' => 409,
|
|
187
185
|
'asset/policy-changed' => 409,
|
|
188
186
|
'asset/reviewed' => 409,
|
|
189
|
-
'support/
|
|
187
|
+
'support-ticket/not-found' => 404,
|
|
190
188
|
'asset/duplicate-external-id' => 409,
|
|
191
189
|
'folder/not-found' => 404,
|
|
192
190
|
'folder/duplicate-name' => 409,
|
|
193
191
|
'folder/not-empty' => 409,
|
|
194
192
|
'folder/parent-cycle' => 409,
|
|
195
|
-
'lens/
|
|
193
|
+
'lens/not-found' => 404,
|
|
194
|
+
'lens/identifier-taken' => 409,
|
|
196
195
|
'lens/identifier-reserved' => 409,
|
|
197
196
|
'hash-list/not-found' => 404,
|
|
198
|
-
'hash-list/
|
|
197
|
+
'hash-list-entry/not-found' => 404,
|
|
199
198
|
'hash-list/limit-reached' => 409,
|
|
200
|
-
'hash-list/
|
|
199
|
+
'hash-list-entry/limit-reached' => 409,
|
|
201
200
|
'hash-list/asset-not-hashed' => 409,
|
|
202
201
|
'policy/not-found' => 404,
|
|
203
202
|
'policy/revision-conflict' => 409,
|
|
@@ -211,18 +210,16 @@ module CalmLens
|
|
|
211
210
|
'upload-preset/not-found' => 404,
|
|
212
211
|
'upload-preset/disabled' => 403,
|
|
213
212
|
'upload-preset/token-required' => 401,
|
|
214
|
-
'
|
|
215
|
-
'upload/website-rate-limit-
|
|
216
|
-
'upload-preset/content-type-not-allowed' => 415,
|
|
217
|
-
'upload-preset/too-large' => 413,
|
|
213
|
+
'remote-url/not-allowed' => 403,
|
|
214
|
+
'upload/website-rate-limit-reached' => 429,
|
|
218
215
|
'upload-preset/daily-limit-reached' => 429,
|
|
219
216
|
'upload-preset/origin-not-allowed' => 403,
|
|
220
217
|
'upload-preset/field-not-allowed' => 400,
|
|
221
218
|
'upload-token/invalid' => 401,
|
|
222
219
|
'upload-token/expired' => 401,
|
|
223
220
|
'upload-token/used' => 409,
|
|
224
|
-
'
|
|
225
|
-
'
|
|
221
|
+
'remote-url/invalid' => 400,
|
|
222
|
+
'remote-url/unavailable' => 422,
|
|
226
223
|
'appeal/not-found' => 404,
|
|
227
224
|
'appeal/judgment-not-found' => 404,
|
|
228
225
|
'appeal/asset-not-found' => 404,
|
|
@@ -230,7 +227,6 @@ module CalmLens
|
|
|
230
227
|
'appeal/already-reviewed' => 409,
|
|
231
228
|
'appeal/decision-changed' => 409,
|
|
232
229
|
'appeal/invalid-action' => 400,
|
|
233
|
-
'appeal/safety-held' => 409,
|
|
234
230
|
'appeal/content-unavailable' => 409,
|
|
235
231
|
}.freeze
|
|
236
232
|
end
|
data/lib/calmlens/errors.rb
CHANGED
|
@@ -70,12 +70,12 @@ module CalmLens
|
|
|
70
70
|
RETRYABLE_CODES = [SdkErrorCodes::TRANSPORT_ERROR, SdkErrorCodes::TIMEOUT,
|
|
71
71
|
ErrorCodes::IDEMPOTENT_REQUEST_IN_PROGRESS].freeze
|
|
72
72
|
# HTTP statuses that may succeed when the same request is repeated.
|
|
73
|
-
RETRYABLE_STATUSES = [429, 502, 503, 504].freeze
|
|
73
|
+
RETRYABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
74
74
|
private_constant :RETRYABLE_CODES, :RETRYABLE_STATUSES
|
|
75
75
|
|
|
76
76
|
# @return [Boolean] true when repeating the same request may succeed: connection failures
|
|
77
77
|
# (`sdk/transport-error`), timeouts (`sdk/timeout`, including {Assets#wait_for_decision}),
|
|
78
|
-
# `idempotency/request-in-progress`, throttling (429)
|
|
78
|
+
# `idempotency/request-in-progress`, throttling (429), 500, 502, 503 and 504. Everything else,
|
|
79
79
|
# including other 5xx statuses, is false. Repeat a write only with the same idempotency key.
|
|
80
80
|
def retryable?
|
|
81
81
|
RETRYABLE_CODES.include?(code) || RETRYABLE_STATUSES.include?(status)
|