calmlens 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/LICENSE +21 -0
- data/README.md +222 -0
- data/lib/calmlens/assets.rb +328 -0
- data/lib/calmlens/cdn.rb +63 -0
- data/lib/calmlens/error_codes.rb +133 -0
- data/lib/calmlens/errors.rb +154 -0
- data/lib/calmlens/events.rb +46 -0
- data/lib/calmlens/folders.rb +63 -0
- data/lib/calmlens/internal.rb +141 -0
- data/lib/calmlens/lenses.rb +16 -0
- data/lib/calmlens/resource.rb +51 -0
- data/lib/calmlens/response.rb +82 -0
- data/lib/calmlens/server_client.rb +134 -0
- data/lib/calmlens/session.rb +246 -0
- data/lib/calmlens/testing/fake_api.rb +570 -0
- data/lib/calmlens/testing.rb +141 -0
- data/lib/calmlens/text.rb +29 -0
- data/lib/calmlens/transforms.rb +104 -0
- data/lib/calmlens/transport.rb +129 -0
- data/lib/calmlens/upload_body.rb +149 -0
- data/lib/calmlens/upload_tokens.rb +52 -0
- data/lib/calmlens/version.rb +6 -0
- data/lib/calmlens/webhooks.rb +122 -0
- data/lib/calmlens.rb +26 -0
- data/sig/calmlens.rbs +273 -0
- data/sig/error_codes.rbs +67 -0
- data/sig/types.rbs +682 -0
- metadata +75 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'openssl'
|
|
5
|
+
|
|
6
|
+
# Webhook header names, signature tolerance, event type and verification helpers.
|
|
7
|
+
module CalmLens
|
|
8
|
+
# Header carrying the delivery ID. Retries of one delivery repeat it: use it to skip duplicates.
|
|
9
|
+
WEBHOOK_DELIVERY_HEADER = 'x-calmlens-delivery'
|
|
10
|
+
# Header carrying the Unix time (seconds) the delivery was signed.
|
|
11
|
+
WEBHOOK_TIMESTAMP_HEADER = 'x-calmlens-timestamp'
|
|
12
|
+
# Header carrying `v1=<hex HMAC-SHA256>` signatures, comma-separated.
|
|
13
|
+
WEBHOOK_SIGNATURE_HEADER = 'x-calmlens-signature'
|
|
14
|
+
# Deliveries whose timestamp is further than this from the receiver's clock are rejected.
|
|
15
|
+
WEBHOOK_SIGNATURE_TOLERANCE_SECONDS = 300
|
|
16
|
+
|
|
17
|
+
# A verified webhook delivery: `{id, type, createdAt, projectId, data}`, where `data` holds the
|
|
18
|
+
# event's `severity`, `title`, `description`, `assetId`, `asset` and `facts`.
|
|
19
|
+
#
|
|
20
|
+
# @!attribute id [String] the event ID.
|
|
21
|
+
# @!attribute type [String] such as `asset.approved`. New types may appear: ignore unknown ones.
|
|
22
|
+
# @!attribute created_at [Integer] milliseconds since the epoch.
|
|
23
|
+
# @!attribute project_id [String]
|
|
24
|
+
# @!attribute data [Hash{String => Object}] the event fields, with the API's camelCase keys.
|
|
25
|
+
WebhookEvent = Struct.new(:id, :type, :created_at, :project_id, :data, keyword_init: true) do
|
|
26
|
+
# @return [String, nil]
|
|
27
|
+
def asset_id = data['assetId']
|
|
28
|
+
|
|
29
|
+
# @return [Array<Hash>] `{'key', 'value', 'url'?}` entries registered for the event type.
|
|
30
|
+
def facts = data['facts'].is_a?(Array) ? data['facts'] : []
|
|
31
|
+
|
|
32
|
+
# @return [Boolean] true for a test send from the portal or CLI.
|
|
33
|
+
def test? = facts.any? { |fact| fact.is_a?(Hash) && fact['key'] == 'test' && fact['value'] == true }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Webhook signing and verification. Always verify the exact bytes received: read the raw body
|
|
37
|
+
# (`request.raw_post` in Rails, `request.body.read` in Rack and Sinatra) before parsing it.
|
|
38
|
+
module Webhooks
|
|
39
|
+
module_function
|
|
40
|
+
|
|
41
|
+
# Builds the `x-calmlens-signature` value for a body, for example to test your receiver.
|
|
42
|
+
# @param payload [String] the raw body
|
|
43
|
+
# @param timestamp [String, Integer] Unix seconds
|
|
44
|
+
# @param secret [String]
|
|
45
|
+
# @return [String] `v1=<hex>`
|
|
46
|
+
def signature_header(payload:, timestamp:, secret:)
|
|
47
|
+
raw = raw_body(payload)
|
|
48
|
+
raise Internal.invalid('secret must be a non-empty String') unless secret.is_a?(String) && !secret.empty?
|
|
49
|
+
|
|
50
|
+
"v1=#{OpenSSL::HMAC.hexdigest('SHA256', secret, "#{timestamp.to_s.b}.".b + raw)}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Checks a delivery's signature and freshness in constant time.
|
|
54
|
+
# @param payload [String] the raw request body, exactly as received
|
|
55
|
+
# @param signature [String, nil] the `x-calmlens-signature` header
|
|
56
|
+
# @param timestamp [String, nil] the `x-calmlens-timestamp` header
|
|
57
|
+
# @param secret [String] the webhook's signing secret
|
|
58
|
+
# @param tolerance_seconds [Integer]
|
|
59
|
+
# @param now [Integer] Unix seconds
|
|
60
|
+
# @return [Boolean]
|
|
61
|
+
# @raise [InvalidArgumentError] when payload is not a String, such as an already parsed Hash.
|
|
62
|
+
def verify(payload:, signature:, timestamp:, secret:, tolerance_seconds: WEBHOOK_SIGNATURE_TOLERANCE_SECONDS,
|
|
63
|
+
now: Time.now.to_i)
|
|
64
|
+
raw = raw_body(payload)
|
|
65
|
+
stamp = timestamp.to_s.strip
|
|
66
|
+
return false if !secret.is_a?(String) || secret.empty? || signature.to_s.empty? || !stamp.match?(/\A[0-9]+\z/)
|
|
67
|
+
return false if (now.floor - stamp.to_i).abs > tolerance_seconds
|
|
68
|
+
|
|
69
|
+
expected = signature_header(payload: raw, timestamp: stamp, secret: secret).delete_prefix('v1=')
|
|
70
|
+
signature.to_s.split(',').map(&:strip).select { |entry| entry.start_with?('v1=') }.any? do |entry|
|
|
71
|
+
candidate = entry.delete_prefix('v1=').downcase
|
|
72
|
+
candidate.bytesize == expected.bytesize && OpenSSL.fixed_length_secure_compare(candidate, expected)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Verifies a delivery, then parses it.
|
|
77
|
+
# @param (see #verify)
|
|
78
|
+
# @return [WebhookEvent]
|
|
79
|
+
# @raise [WebhookVerificationError] `sdk/invalid-webhook-signature` when verification fails, or
|
|
80
|
+
# `sdk/invalid-webhook-payload` when the verified body is not an event.
|
|
81
|
+
def parse_event(payload:, signature:, timestamp:, secret:,
|
|
82
|
+
tolerance_seconds: WEBHOOK_SIGNATURE_TOLERANCE_SECONDS, now: Time.now.to_i)
|
|
83
|
+
unless verify(payload: payload, signature: signature, timestamp: timestamp, secret: secret,
|
|
84
|
+
tolerance_seconds: tolerance_seconds, now: now)
|
|
85
|
+
raise WebhookVerificationError.new(SdkErrorCodes::INVALID_WEBHOOK_SIGNATURE,
|
|
86
|
+
'Webhook signature or timestamp did not verify')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
decode_event(payload)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def decode_event(payload)
|
|
93
|
+
body = JSON.parse(payload.dup.force_encoding(Encoding::UTF_8))
|
|
94
|
+
unless body.is_a?(Hash) && body['id'].is_a?(String) && body['type'].is_a?(String) && body['createdAt'].is_a?(Integer) &&
|
|
95
|
+
body['projectId'].is_a?(String) && body['data'].is_a?(Hash)
|
|
96
|
+
raise WebhookVerificationError.new(SdkErrorCodes::INVALID_WEBHOOK_PAYLOAD, 'Webhook body is not a CalmLens event')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
WebhookEvent.new(id: body['id'], type: body['type'], created_at: body['createdAt'],
|
|
100
|
+
project_id: body['projectId'], data: body['data'])
|
|
101
|
+
rescue JSON::ParserError, EncodingError => e
|
|
102
|
+
raise WebhookVerificationError.new(SdkErrorCodes::INVALID_WEBHOOK_PAYLOAD, 'Webhook body is not JSON'), cause: e
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def raw_body(payload)
|
|
106
|
+
return payload.b if payload.is_a?(String)
|
|
107
|
+
|
|
108
|
+
raise Internal.invalid("Webhook payload must be the raw body String, not a #{payload.class}. " \
|
|
109
|
+
'Read request.raw_post (Rails) or request.body.read (Rack, Sinatra) before parsing.')
|
|
110
|
+
end
|
|
111
|
+
private_class_method :decode_event, :raw_body
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# @see Webhooks.signature_header
|
|
115
|
+
def self.create_webhook_signature_header(...) = Webhooks.signature_header(...)
|
|
116
|
+
|
|
117
|
+
# @see Webhooks.verify
|
|
118
|
+
def self.verify_webhook_signature(...) = Webhooks.verify(...)
|
|
119
|
+
|
|
120
|
+
# @see Webhooks.parse_event
|
|
121
|
+
def self.parse_webhook_event(...) = Webhooks.parse_event(...)
|
|
122
|
+
end
|
data/lib/calmlens.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# CalmLens server-side SDK: moderation, uploads, events, webhooks and CDN delivery.
|
|
4
|
+
# Start with {CalmLens::ServerClient}; see {CalmLens::Webhooks} to receive deliveries.
|
|
5
|
+
module CalmLens
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
require_relative 'calmlens/version'
|
|
9
|
+
require_relative 'calmlens/error_codes'
|
|
10
|
+
require_relative 'calmlens/errors'
|
|
11
|
+
require_relative 'calmlens/internal'
|
|
12
|
+
require_relative 'calmlens/transforms'
|
|
13
|
+
require_relative 'calmlens/response'
|
|
14
|
+
require_relative 'calmlens/transport'
|
|
15
|
+
require_relative 'calmlens/upload_body'
|
|
16
|
+
require_relative 'calmlens/session'
|
|
17
|
+
require_relative 'calmlens/resource'
|
|
18
|
+
require_relative 'calmlens/assets'
|
|
19
|
+
require_relative 'calmlens/folders'
|
|
20
|
+
require_relative 'calmlens/text'
|
|
21
|
+
require_relative 'calmlens/upload_tokens'
|
|
22
|
+
require_relative 'calmlens/events'
|
|
23
|
+
require_relative 'calmlens/cdn'
|
|
24
|
+
require_relative 'calmlens/lenses'
|
|
25
|
+
require_relative 'calmlens/server_client'
|
|
26
|
+
require_relative 'calmlens/webhooks'
|
data/sig/calmlens.rbs
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Public API of the calmlens gem. Request and response shapes are in sig/types.rbs and error codes
|
|
2
|
+
# in sig/error_codes.rbs.
|
|
3
|
+
module CalmLens
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
WEBHOOK_DELIVERY_HEADER: String
|
|
7
|
+
WEBHOOK_TIMESTAMP_HEADER: String
|
|
8
|
+
WEBHOOK_SIGNATURE_HEADER: String
|
|
9
|
+
WEBHOOK_SIGNATURE_TOLERANCE_SECONDS: Integer
|
|
10
|
+
|
|
11
|
+
module SdkErrorCodes
|
|
12
|
+
INVALID_ARGUMENT: String
|
|
13
|
+
INVALID_CONFIG: String
|
|
14
|
+
TRANSPORT_ERROR: String
|
|
15
|
+
TIMEOUT: String
|
|
16
|
+
INVALID_RESPONSE: String
|
|
17
|
+
HTTP_ERROR: String
|
|
18
|
+
RESPONSE_TOO_LARGE: String
|
|
19
|
+
INVALID_WEBHOOK_SIGNATURE: String
|
|
20
|
+
INVALID_WEBHOOK_PAYLOAD: String
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Error < StandardError
|
|
24
|
+
attr_reader code: String
|
|
25
|
+
# Extra details from the API error body (any JSON), plus `"network" => true` for API errors.
|
|
26
|
+
attr_reader details: Hash[String, json_value]
|
|
27
|
+
attr_reader status: Integer?
|
|
28
|
+
attr_reader request_id: String?
|
|
29
|
+
attr_reader headers: Hash[String, String]
|
|
30
|
+
def initialize: (String code, ?String? message, ?details: Hash[String, json_value], ?status: Integer?,
|
|
31
|
+
?headers: Hash[String, String], ?request_id: String?) -> void
|
|
32
|
+
def retry_after: () -> Float?
|
|
33
|
+
def not_found?: () -> bool
|
|
34
|
+
def retryable?: () -> bool
|
|
35
|
+
def self.build: (String code, ?String? message, ?status: Integer?, ?details: Hash[String, json_value],
|
|
36
|
+
?headers: Hash[String, String], ?request_id: String?) -> Error
|
|
37
|
+
end
|
|
38
|
+
class APIError < Error end
|
|
39
|
+
class BadRequestError < APIError end
|
|
40
|
+
class AuthenticationError < APIError end
|
|
41
|
+
class PermissionDeniedError < APIError end
|
|
42
|
+
class NotFoundError < APIError end
|
|
43
|
+
class ConflictError < APIError end
|
|
44
|
+
class RateLimitError < APIError end
|
|
45
|
+
class ServerError < APIError end
|
|
46
|
+
class ConnectionError < Error end
|
|
47
|
+
class TimeoutError < ConnectionError end
|
|
48
|
+
class InvalidResponseError < Error end
|
|
49
|
+
class InvalidArgumentError < Error end
|
|
50
|
+
class ConfigurationError < InvalidArgumentError end
|
|
51
|
+
class WebhookVerificationError < Error end
|
|
52
|
+
|
|
53
|
+
class Response
|
|
54
|
+
attr_reader status: Integer
|
|
55
|
+
attr_reader headers: Hash[String, String]
|
|
56
|
+
# Give `body:` for a complete body, or a reader block that yields each String chunk to the
|
|
57
|
+
# block it receives. RBS cannot describe a block that takes a block, hence `(?)`.
|
|
58
|
+
def initialize: (status: Integer | String, ?headers: Hash[String | Symbol, String | Array[String]], ?body: String?)
|
|
59
|
+
?{ (?) -> void } -> void
|
|
60
|
+
def request_id: () -> String?
|
|
61
|
+
def content_type: () -> String?
|
|
62
|
+
def content_length: () -> Integer?
|
|
63
|
+
def success?: () -> bool
|
|
64
|
+
def each_chunk: () { (String chunk) -> void } -> nil
|
|
65
|
+
| () -> Enumerator[String, nil]
|
|
66
|
+
def body: (?limit: Integer?) -> String
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
module Transport
|
|
70
|
+
class Request < Struct[untyped]
|
|
71
|
+
attr_accessor method: String
|
|
72
|
+
attr_accessor url: String
|
|
73
|
+
attr_accessor headers: Hash[String, String]
|
|
74
|
+
attr_accessor body: (String | _Reader)?
|
|
75
|
+
attr_accessor open_timeout: number
|
|
76
|
+
attr_accessor timeout: number
|
|
77
|
+
def initialize: (method: String, url: String, headers: Hash[String, String], body: (String | _Reader)?,
|
|
78
|
+
open_timeout: number, timeout: number) -> void
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
interface _Reader
|
|
82
|
+
def read: (?Integer? length, ?String? buffer) -> String?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# A custom transport receives each request and either yields a {Response} to the block (and
|
|
86
|
+
# returns the block's result) or returns a response: a {Response}, or any object with `status`
|
|
87
|
+
# (or `code`), `headers` (or `to_hash`) and `body`. The return value is untyped because the
|
|
88
|
+
# client only inspects it when the transport did not yield.
|
|
89
|
+
interface _Transport
|
|
90
|
+
def call: (Request request) { (Response response) -> untyped } -> untyped
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
class NetHTTP
|
|
94
|
+
def initialize: () -> void
|
|
95
|
+
# Returns the block's result.
|
|
96
|
+
def call: [T] (Request request) { (Response response) -> T } -> T?
|
|
97
|
+
def close: () -> void
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ServerClient
|
|
102
|
+
DEFAULT_BASE_URL: String
|
|
103
|
+
DEFAULT_CDN_BASE_URL: String
|
|
104
|
+
attr_reader assets: Assets
|
|
105
|
+
attr_reader folders: Folders
|
|
106
|
+
attr_reader text: Text
|
|
107
|
+
attr_reader upload_tokens: UploadTokens
|
|
108
|
+
attr_reader events: Events
|
|
109
|
+
attr_reader cdn: CDN
|
|
110
|
+
attr_reader project_id: String
|
|
111
|
+
attr_reader base_url: String
|
|
112
|
+
attr_reader cdn_base_url: String
|
|
113
|
+
def initialize: (api_key: String, project_id: String, ?base_url: String, ?cdn_base_url: String, ?timeout: number,
|
|
114
|
+
?open_timeout: number, ?max_retries: Integer, ?retry_base_delay: number,
|
|
115
|
+
?transport: Transport::_Transport?) -> void
|
|
116
|
+
def close: () -> nil
|
|
117
|
+
def ensure_lenses: (ensure_lenses_options options) -> Array[lens]
|
|
118
|
+
def ensure_lens: (ensure_lens_options options) -> lens
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
class Resource
|
|
122
|
+
# Resources are created by {ServerClient}; the session is internal.
|
|
123
|
+
private def initialize: (untyped session) -> void
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
class Assets < Resource
|
|
127
|
+
# Moderation statuses that carry a decision.
|
|
128
|
+
DECIDED_MODERATION_STATUSES: Array[String]
|
|
129
|
+
# Processing statuses after which no decision arrives without a retry.
|
|
130
|
+
FINISHED_PROCESSING_STATUSES: Array[String]
|
|
131
|
+
|
|
132
|
+
def search: (asset_search_options options) -> asset_search_response
|
|
133
|
+
def list: (?list_assets_options options) -> asset_page
|
|
134
|
+
def each: (?list_assets_options options) { (asset) -> void } -> nil
|
|
135
|
+
| (?list_assets_options options) -> Enumerator[asset, nil]
|
|
136
|
+
def list_children: (list_asset_children_options options) -> asset_page
|
|
137
|
+
def each_child: (list_asset_children_options options) { (asset) -> void } -> nil
|
|
138
|
+
| (list_asset_children_options options) -> Enumerator[asset, nil]
|
|
139
|
+
def retrieve: (asset_id_options options) -> asset
|
|
140
|
+
def wait_for_decision: (wait_for_decision_options options) -> asset
|
|
141
|
+
def update: (update_asset_options options) -> asset
|
|
142
|
+
def retry: (asset_id_options options) -> asset
|
|
143
|
+
def delete_content: (asset_id_options options) -> asset
|
|
144
|
+
def delete: (asset_id_options options) -> nil
|
|
145
|
+
def move: (move_asset_options options) -> asset
|
|
146
|
+
def create_signed_url: (create_signed_url_options options) -> signed_url
|
|
147
|
+
def create: (upload_options options) -> asset
|
|
148
|
+
def moderate: (upload_options options) -> asset
|
|
149
|
+
def upload_part: (upload_part_options options) -> asset
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
class Folders < Resource
|
|
153
|
+
def list: (?list_folders_options options) -> Array[folder]
|
|
154
|
+
def create: (create_folder_options options) -> folder
|
|
155
|
+
def retrieve: (folder_id_options options) -> folder
|
|
156
|
+
def update: (update_folder_options options) -> folder
|
|
157
|
+
def move: (move_folder_options options) -> folder
|
|
158
|
+
def delete: (folder_id_options options) -> nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
class UploadTokens < Resource
|
|
162
|
+
TOKEN_PREFIX: String
|
|
163
|
+
SIGNING_CONTEXT: String
|
|
164
|
+
DEFAULT_TTL_SECONDS: Integer
|
|
165
|
+
MAX_TTL_SECONDS: Integer
|
|
166
|
+
private def initialize: (untyped session, String api_key) -> void
|
|
167
|
+
def sign: (sign_upload_token_options options) -> signed_upload_token
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
class Text < Resource
|
|
171
|
+
private def initialize: (untyped session, Assets assets) -> void
|
|
172
|
+
def moderate: (moderate_text_options options) -> asset
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
class Events < Resource
|
|
176
|
+
def list: (?list_events_options options) -> events_page
|
|
177
|
+
def each_page: (?list_events_options options) { (events_page) -> void } -> nil
|
|
178
|
+
| (?list_events_options options) -> Enumerator[events_page, nil]
|
|
179
|
+
def each: (?list_events_options options) { (event) -> void } -> nil
|
|
180
|
+
| (?list_events_options options) -> Enumerator[event, nil]
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
class CDN < Resource
|
|
184
|
+
def get_asset_url: (asset_url_options options) -> String
|
|
185
|
+
def serve: (serve_options options) -> Response
|
|
186
|
+
| [T] (serve_options options) { (Response) -> T } -> T
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
class WebhookEvent < Struct[untyped]
|
|
190
|
+
attr_accessor id: String
|
|
191
|
+
attr_accessor type: event_type
|
|
192
|
+
# Milliseconds since the Unix epoch.
|
|
193
|
+
attr_accessor created_at: Integer
|
|
194
|
+
attr_accessor project_id: String
|
|
195
|
+
attr_accessor data: webhook_event_data
|
|
196
|
+
def initialize: (id: String, type: event_type, created_at: Integer, project_id: String, data: webhook_event_data) -> void
|
|
197
|
+
def asset_id: () -> String?
|
|
198
|
+
def facts: () -> Array[event_fact]
|
|
199
|
+
def test?: () -> bool
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
module Webhooks
|
|
203
|
+
def self.signature_header: (payload: String, timestamp: String | Integer, secret: String) -> String
|
|
204
|
+
def self.verify: (payload: String, signature: String?, timestamp: String?, secret: String, ?tolerance_seconds: Integer,
|
|
205
|
+
?now: number) -> bool
|
|
206
|
+
def self.parse_event: (payload: String, signature: String?, timestamp: String?, secret: String,
|
|
207
|
+
?tolerance_seconds: Integer, ?now: number) -> WebhookEvent
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def self.create_webhook_signature_header: (payload: String, timestamp: String | Integer, secret: String) -> String
|
|
211
|
+
def self.verify_webhook_signature: (payload: String, signature: String?, timestamp: String?, secret: String,
|
|
212
|
+
?tolerance_seconds: Integer, ?now: number) -> bool
|
|
213
|
+
def self.parse_webhook_event: (payload: String, signature: String?, timestamp: String?, secret: String,
|
|
214
|
+
?tolerance_seconds: Integer, ?now: number) -> WebhookEvent
|
|
215
|
+
|
|
216
|
+
# require 'calmlens/testing'
|
|
217
|
+
module Testing
|
|
218
|
+
# The mock's in-memory records. Change them to arrange a test, such as setting an asset's `moderation.status`.
|
|
219
|
+
class State < Struct[untyped]
|
|
220
|
+
attr_accessor assets: Hash[String, asset]
|
|
221
|
+
attr_accessor folders: Hash[String, folder]
|
|
222
|
+
# Stored bytes by asset ID, with each part's bytes by part number.
|
|
223
|
+
attr_accessor content: Hash[String, { "type" => String, "parts" => Hash[Integer, String] }]
|
|
224
|
+
attr_accessor events: Array[event]
|
|
225
|
+
attr_accessor lenses: Hash[String, lens]
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
class MockServerClient < ServerClient
|
|
230
|
+
class Request < Struct[untyped]
|
|
231
|
+
attr_accessor method: String
|
|
232
|
+
attr_accessor url: String
|
|
233
|
+
attr_accessor path: String
|
|
234
|
+
attr_accessor query: Hash[String, String]
|
|
235
|
+
attr_accessor headers: Hash[String, String]
|
|
236
|
+
attr_accessor body: String?
|
|
237
|
+
def json: () -> json_value
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
interface _Clock
|
|
241
|
+
# The current time in milliseconds since the Unix epoch.
|
|
242
|
+
def call: () -> Integer
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
interface _IdFactory
|
|
246
|
+
def call: () -> String
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Where a mock upload ends up: `pending` (still processing), `approved` or `rejected`
|
|
250
|
+
# (processing done with that decision), or `failed` (processing failed, retryable).
|
|
251
|
+
type outcome = "pending" | "approved" | "rejected" | "failed"
|
|
252
|
+
|
|
253
|
+
# An outcome, fields to deep-merge into the asset, or a callable that receives a copy of the
|
|
254
|
+
# asset and returns either.
|
|
255
|
+
type analyzed_outcome = outcome | Hash[String, json_input] | _AnalyzedOutcome
|
|
256
|
+
|
|
257
|
+
interface _AnalyzedOutcome
|
|
258
|
+
def call: (asset asset) -> (String | Hash[String, json_input])
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def initialize: (?project_id: String, ?base_url: String, ?cdn_base_url: String, ?now: _Clock, ?create_id: _IdFactory,
|
|
262
|
+
?queued_outcome: outcome, ?analyzed_outcome: analyzed_outcome, ?timeout: number,
|
|
263
|
+
?open_timeout: number, ?max_retries: Integer, ?retry_base_delay: number) -> void
|
|
264
|
+
def state: () -> Testing::State
|
|
265
|
+
# `body` is any JSON value, encoded as the response body.
|
|
266
|
+
def enqueue: (body: json_input, ?status: Integer, ?headers: Hash[String, String]) -> self
|
|
267
|
+
def enqueue_raw: (body: String, ?status: Integer, ?headers: Hash[String, String]) -> self
|
|
268
|
+
def enqueue_error: (Exception error) -> self
|
|
269
|
+
def enqueue_event: (mock_event_input event) -> event
|
|
270
|
+
def requests: () -> Array[Request]
|
|
271
|
+
def remaining_responses: () -> Integer
|
|
272
|
+
end
|
|
273
|
+
end
|
data/sig/error_codes.rbs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Mirrors lib/calmlens/error_codes.rb, which packages/sdk-contract/generate.ts generates from
|
|
2
|
+
# packages/shared/src/PublicErrorCodes.ts. test/signature_test.rb fails when the two differ.
|
|
3
|
+
module CalmLens
|
|
4
|
+
module ErrorCodes
|
|
5
|
+
INVALID_CURSOR: String
|
|
6
|
+
MISSING_TOKEN: String
|
|
7
|
+
INVALID_API_KEY: String
|
|
8
|
+
API_KEY_PROJECT_MISMATCH: String
|
|
9
|
+
API_KEY_NOT_ACCEPTED: String
|
|
10
|
+
FILE_TOO_LARGE: String
|
|
11
|
+
IMAGE_REQUIRES_MULTIPART_UPLOAD: String
|
|
12
|
+
MULTIPART_IMAGE_TOO_SMALL: String
|
|
13
|
+
MULTIPART_IMAGE_TOO_LARGE: String
|
|
14
|
+
IMAGE_EXCEEDED_MAX_DIMENSIONS: String
|
|
15
|
+
UPLOAD_LIMIT_EXCEEDED: String
|
|
16
|
+
SPEND_LIMIT_REACHED: String
|
|
17
|
+
INVALID_UPLOAD_MIME_TYPE: String
|
|
18
|
+
INVALID_EVENT_CURSOR: String
|
|
19
|
+
INVALID_UPLOAD_PAYLOAD: String
|
|
20
|
+
USE_UPLOAD_PART: String
|
|
21
|
+
UPLOAD_NOT_OPEN: String
|
|
22
|
+
INVALID_UPLOAD_PART_NUMBER: String
|
|
23
|
+
UPLOAD_MISSING: String
|
|
24
|
+
UPLOAD_SIZE_MISMATCH: String
|
|
25
|
+
INVALID_IDEMPOTENCY_KEY: String
|
|
26
|
+
IDEMPOTENCY_KEY_REUSED: String
|
|
27
|
+
IDEMPOTENT_REQUEST_IN_PROGRESS: String
|
|
28
|
+
ASSET_NOT_FOUND: String
|
|
29
|
+
PROJECT_NOT_FOUND: String
|
|
30
|
+
ASSET_UPDATE_FAILED: String
|
|
31
|
+
ASSET_SAFETY_HELD: String
|
|
32
|
+
ASSET_NOT_RETRYABLE: String
|
|
33
|
+
ASSET_CONTENT_UNAVAILABLE: String
|
|
34
|
+
SUPPORT_TICKET_NOT_FOUND: String
|
|
35
|
+
DUPLICATE_EXTERNAL_ID: String
|
|
36
|
+
FOLDER_NOT_FOUND: String
|
|
37
|
+
DUPLICATE_FOLDER_NAME: String
|
|
38
|
+
FOLDER_NOT_EMPTY: String
|
|
39
|
+
FOLDER_PARENT_CYCLE: String
|
|
40
|
+
LENS_VERSION_CONFLICT: String
|
|
41
|
+
UPLOAD_PRESET_NOT_FOUND: String
|
|
42
|
+
UPLOAD_PRESET_DISABLED: String
|
|
43
|
+
UPLOAD_PRESET_TOKEN_REQUIRED: String
|
|
44
|
+
REMOTE_URL_NOT_ALLOWED: String
|
|
45
|
+
WEBSITE_RATE_LIMIT_EXCEEDED: String
|
|
46
|
+
UPLOAD_PRESET_CONTENT_TYPE_NOT_ALLOWED: String
|
|
47
|
+
UPLOAD_PRESET_TOO_LARGE: String
|
|
48
|
+
UPLOAD_PRESET_DAILY_LIMIT_REACHED: String
|
|
49
|
+
UPLOAD_PRESET_ORIGIN_NOT_ALLOWED: String
|
|
50
|
+
UPLOAD_PRESET_FIELD_NOT_ALLOWED: String
|
|
51
|
+
UPLOAD_TOKEN_INVALID: String
|
|
52
|
+
UPLOAD_TOKEN_EXPIRED: String
|
|
53
|
+
UPLOAD_TOKEN_USED: String
|
|
54
|
+
INVALID_REMOTE_URL: String
|
|
55
|
+
REMOTE_URL_UNAVAILABLE: String
|
|
56
|
+
APPEAL_NOT_FOUND: String
|
|
57
|
+
APPEAL_JUDGMENT_NOT_FOUND: String
|
|
58
|
+
APPEAL_ASSET_NOT_FOUND: String
|
|
59
|
+
APPEAL_PERMISSION_DENIED: String
|
|
60
|
+
APPEAL_ALREADY_REVIEWED: String
|
|
61
|
+
APPEAL_DECISION_CHANGED: String
|
|
62
|
+
APPEAL_INVALID_ACTION: String
|
|
63
|
+
APPEAL_SAFETY_HELD: String
|
|
64
|
+
APPEAL_CONTENT_UNAVAILABLE: String
|
|
65
|
+
STATUSES: Hash[String, Integer]
|
|
66
|
+
end
|
|
67
|
+
end
|