apertur-sdk 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 +195 -0
- data/lib/apertur/client.rb +75 -0
- data/lib/apertur/crypto.rb +60 -0
- data/lib/apertur/errors.rb +55 -0
- data/lib/apertur/http_client.rb +195 -0
- data/lib/apertur/resources/destinations.rb +61 -0
- data/lib/apertur/resources/encryption.rb +22 -0
- data/lib/apertur/resources/keys.rb +61 -0
- data/lib/apertur/resources/polling.rb +70 -0
- data/lib/apertur/resources/sessions.rb +112 -0
- data/lib/apertur/resources/stats.rb +20 -0
- data/lib/apertur/resources/upload.rb +98 -0
- data/lib/apertur/resources/uploads.rb +35 -0
- data/lib/apertur/resources/webhooks.rb +85 -0
- data/lib/apertur/signature.rb +79 -0
- data/lib/apertur/version.rb +5 -0
- data/lib/apertur.rb +34 -0
- metadata +68 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Manage API keys within a project.
|
|
6
|
+
class Keys
|
|
7
|
+
# @param http [Apertur::HttpClient]
|
|
8
|
+
def initialize(http)
|
|
9
|
+
@http = http
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# List all API keys for a project.
|
|
13
|
+
#
|
|
14
|
+
# @param project_id [String] the project ID
|
|
15
|
+
# @return [Array<Hash>] list of API keys
|
|
16
|
+
def list(project_id)
|
|
17
|
+
@http.request(:get, "/api/v1/projects/#{project_id}/keys")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a new API key.
|
|
21
|
+
#
|
|
22
|
+
# @param project_id [String] the project ID
|
|
23
|
+
# @param options [Hash] key configuration (e.g. +name+, +scopes+)
|
|
24
|
+
# @return [Hash] the created key, including the plaintext secret (shown only once)
|
|
25
|
+
def create(project_id, **options)
|
|
26
|
+
@http.request(:post, "/api/v1/projects/#{project_id}/keys", body: options)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Update an existing API key.
|
|
30
|
+
#
|
|
31
|
+
# @param project_id [String] the project ID
|
|
32
|
+
# @param key_id [String] the key ID
|
|
33
|
+
# @param options [Hash] fields to update
|
|
34
|
+
# @return [Hash] the updated key
|
|
35
|
+
def update(project_id, key_id, **options)
|
|
36
|
+
@http.request(:patch, "/api/v1/projects/#{project_id}/keys/#{key_id}", body: options)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delete an API key.
|
|
40
|
+
#
|
|
41
|
+
# @param project_id [String] the project ID
|
|
42
|
+
# @param key_id [String] the key ID
|
|
43
|
+
# @return [nil]
|
|
44
|
+
def delete(project_id, key_id)
|
|
45
|
+
@http.request(:delete, "/api/v1/projects/#{project_id}/keys/#{key_id}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Set the destinations and long-polling configuration for a key.
|
|
49
|
+
#
|
|
50
|
+
# @param key_id [String] the key ID
|
|
51
|
+
# @param destination_ids [Array<String>] destination IDs to associate
|
|
52
|
+
# @param long_polling_enabled [Boolean] whether to enable long-polling (default: false)
|
|
53
|
+
# @return [Hash] the updated key-destinations mapping
|
|
54
|
+
def set_destinations(key_id, destination_ids, long_polling_enabled: false)
|
|
55
|
+
body = { destination_ids: destination_ids }
|
|
56
|
+
body[:long_polling_enabled] = long_polling_enabled unless long_polling_enabled.nil?
|
|
57
|
+
@http.request(:put, "/api/v1/keys/#{key_id}/destinations", body: body)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Poll upload sessions for new images and download them.
|
|
6
|
+
#
|
|
7
|
+
# Provides a blocking polling loop ({#poll_and_process}) that fetches,
|
|
8
|
+
# downloads, and acknowledges new images automatically.
|
|
9
|
+
class Polling
|
|
10
|
+
# @param http [Apertur::HttpClient]
|
|
11
|
+
def initialize(http)
|
|
12
|
+
@http = http
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# List pending (un-acknowledged) images in a session.
|
|
16
|
+
#
|
|
17
|
+
# @param uuid [String] the session UUID
|
|
18
|
+
# @return [Hash] poll result containing an +images+ array
|
|
19
|
+
def list(uuid)
|
|
20
|
+
@http.request(:get, "/api/v1/upload-sessions/#{uuid}/poll")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Download an image from a session.
|
|
24
|
+
#
|
|
25
|
+
# @param uuid [String] the session UUID
|
|
26
|
+
# @param image_id [String] the image ID
|
|
27
|
+
# @return [String] raw binary image data
|
|
28
|
+
def download(uuid, image_id)
|
|
29
|
+
@http.request_raw(:get, "/api/v1/upload-sessions/#{uuid}/images/#{image_id}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Acknowledge (mark as processed) an image.
|
|
33
|
+
#
|
|
34
|
+
# @param uuid [String] the session UUID
|
|
35
|
+
# @param image_id [String] the image ID
|
|
36
|
+
# @return [Hash] acknowledgement status
|
|
37
|
+
def ack(uuid, image_id)
|
|
38
|
+
@http.request(:post, "/api/v1/upload-sessions/#{uuid}/images/#{image_id}/ack")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Blocking polling loop that fetches, downloads, and acknowledges images.
|
|
42
|
+
#
|
|
43
|
+
# Calls the provided block for each new image. The loop runs until the
|
|
44
|
+
# calling thread is interrupted or the block raises an exception.
|
|
45
|
+
#
|
|
46
|
+
# @param uuid [String] the session UUID
|
|
47
|
+
# @param interval [Numeric] seconds between poll cycles (default: 3)
|
|
48
|
+
# @yield [image, data] called for each new image
|
|
49
|
+
# @yieldparam image [Hash] image metadata from the poll response
|
|
50
|
+
# @yieldparam data [String] raw binary image data
|
|
51
|
+
# @return [void]
|
|
52
|
+
def poll_and_process(uuid, interval: 3, &handler)
|
|
53
|
+
raise ArgumentError, "A block is required" unless block_given?
|
|
54
|
+
|
|
55
|
+
loop do
|
|
56
|
+
result = list(uuid)
|
|
57
|
+
images = result["images"] || []
|
|
58
|
+
|
|
59
|
+
images.each do |image|
|
|
60
|
+
data = download(uuid, image["id"])
|
|
61
|
+
handler.call(image, data)
|
|
62
|
+
ack(uuid, image["id"])
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sleep(interval)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Manage upload sessions.
|
|
6
|
+
#
|
|
7
|
+
# Upload sessions represent a time-limited context in which one or more
|
|
8
|
+
# images can be uploaded. Each session has a unique UUID, optional password
|
|
9
|
+
# protection, and configurable constraints.
|
|
10
|
+
class Sessions
|
|
11
|
+
# @param http [Apertur::HttpClient]
|
|
12
|
+
def initialize(http)
|
|
13
|
+
@http = http
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Create a new upload session.
|
|
17
|
+
#
|
|
18
|
+
# @param destination_ids [Array<String>, nil] destination IDs to deliver images to
|
|
19
|
+
# @param long_polling [Boolean, nil] whether to enable long-polling on this session
|
|
20
|
+
# @param tags [Array<String>, nil] tags to attach to the session
|
|
21
|
+
# @param expires_in_hours [Integer, nil] hours until the session expires
|
|
22
|
+
# @param expires_at [String, nil] ISO 8601 expiry timestamp
|
|
23
|
+
# @param max_images [Integer, nil] maximum number of images allowed
|
|
24
|
+
# @param allowed_mime_types [Array<String>, nil] allowed MIME types for uploads
|
|
25
|
+
# @param max_image_dimension [Integer, nil] maximum image dimension in pixels
|
|
26
|
+
# @param password [String, nil] optional password to protect the session
|
|
27
|
+
# @return [Hash] the created session details including +uuid+ and +upload_url+
|
|
28
|
+
def create(**options)
|
|
29
|
+
@http.request(:post, "/api/v1/upload-sessions", body: options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Retrieve an existing upload session by UUID.
|
|
33
|
+
#
|
|
34
|
+
# @param uuid [String] the session UUID
|
|
35
|
+
# @return [Hash] session details
|
|
36
|
+
def get(uuid)
|
|
37
|
+
@http.request(:get, "/api/v1/upload/#{uuid}/session")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Update an existing upload session.
|
|
41
|
+
#
|
|
42
|
+
# @param uuid [String] the session UUID
|
|
43
|
+
# @param options [Hash] fields to update (same options as {#create})
|
|
44
|
+
# @return [Hash] the updated session
|
|
45
|
+
def update(uuid, **options)
|
|
46
|
+
@http.request(:patch, "/api/v1/upload-sessions/#{uuid}", body: options)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# List upload sessions with pagination.
|
|
50
|
+
#
|
|
51
|
+
# @param page [Integer, nil] page number
|
|
52
|
+
# @param page_size [Integer, nil] number of results per page
|
|
53
|
+
# @return [Hash] paginated list with +data+ and pagination metadata
|
|
54
|
+
def list(**params)
|
|
55
|
+
query = {}
|
|
56
|
+
query["page"] = params[:page].to_s if params[:page]
|
|
57
|
+
query["pageSize"] = params[:page_size].to_s if params[:page_size]
|
|
58
|
+
@http.request(:get, "/api/v1/sessions", query: query)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# List recent upload sessions.
|
|
62
|
+
#
|
|
63
|
+
# @param limit [Integer, nil] maximum number of sessions to return
|
|
64
|
+
# @return [Array<Hash>] recent sessions
|
|
65
|
+
def recent(**params)
|
|
66
|
+
query = {}
|
|
67
|
+
query["limit"] = params[:limit].to_s if params[:limit]
|
|
68
|
+
@http.request(:get, "/api/v1/sessions/recent", query: query)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the QR code image for an upload session.
|
|
72
|
+
#
|
|
73
|
+
# @param uuid [String] the session UUID
|
|
74
|
+
# @param format [String, nil] image format (e.g. "png", "svg")
|
|
75
|
+
# @param size [Integer, nil] QR code size in pixels
|
|
76
|
+
# @param style [String, nil] QR code style
|
|
77
|
+
# @param fg [String, nil] foreground color
|
|
78
|
+
# @param bg [String, nil] background color
|
|
79
|
+
# @param border_size [Integer, nil] border size in pixels
|
|
80
|
+
# @param border_color [String, nil] border color
|
|
81
|
+
# @return [String] raw binary image data
|
|
82
|
+
def qr(uuid, **options)
|
|
83
|
+
query = {}
|
|
84
|
+
query["format"] = options[:format] if options[:format]
|
|
85
|
+
query["size"] = options[:size].to_s if options[:size]
|
|
86
|
+
query["style"] = options[:style] if options[:style]
|
|
87
|
+
query["fg"] = options[:fg] if options[:fg]
|
|
88
|
+
query["bg"] = options[:bg] if options[:bg]
|
|
89
|
+
query["borderSize"] = options[:border_size].to_s if options[:border_size]
|
|
90
|
+
query["borderColor"] = options[:border_color] if options[:border_color]
|
|
91
|
+
@http.request_raw(:get, "/api/v1/upload-sessions/#{uuid}/qr", query: query)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Verify a session password.
|
|
95
|
+
#
|
|
96
|
+
# @param uuid [String] the session UUID
|
|
97
|
+
# @param password [String] the password to verify
|
|
98
|
+
# @return [Hash] result with +valid+ boolean
|
|
99
|
+
def verify_password(uuid, password)
|
|
100
|
+
@http.request(:post, "/api/v1/upload/#{uuid}/verify-password", body: { password: password })
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Get the delivery status for a session.
|
|
104
|
+
#
|
|
105
|
+
# @param uuid [String] the session UUID
|
|
106
|
+
# @return [Array<Hash>] delivery status records
|
|
107
|
+
def delivery_status(uuid)
|
|
108
|
+
@http.request(:get, "/api/v1/upload-sessions/#{uuid}/delivery-status")
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Retrieve account usage statistics.
|
|
6
|
+
class Stats
|
|
7
|
+
# @param http [Apertur::HttpClient]
|
|
8
|
+
def initialize(http)
|
|
9
|
+
@http = http
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Get current account statistics.
|
|
13
|
+
#
|
|
14
|
+
# @return [Hash] usage statistics (uploads, sessions, storage, etc.)
|
|
15
|
+
def get
|
|
16
|
+
@http.request(:get, "/api/v1/stats")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Upload images to a session.
|
|
6
|
+
#
|
|
7
|
+
# Supports both plaintext multipart uploads and client-side encrypted
|
|
8
|
+
# uploads using AES-256-GCM with RSA-OAEP key wrapping.
|
|
9
|
+
class Upload
|
|
10
|
+
# @param http [Apertur::HttpClient]
|
|
11
|
+
def initialize(http)
|
|
12
|
+
@http = http
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Upload an image to a session via multipart/form-data.
|
|
16
|
+
#
|
|
17
|
+
# @param uuid [String] the session UUID
|
|
18
|
+
# @param file [String, IO] a file path (String), an IO-like object responding
|
|
19
|
+
# to +read+, or raw image bytes (binary String)
|
|
20
|
+
# @param filename [String] the filename to send (default: "image.jpg")
|
|
21
|
+
# @param mime_type [String] the MIME type of the image (default: "image/jpeg")
|
|
22
|
+
# @param source [String, nil] an optional source identifier
|
|
23
|
+
# @param password [String, nil] session password if the session is protected
|
|
24
|
+
# @return [Hash] upload result
|
|
25
|
+
def image(uuid, file, filename: "image.jpg", mime_type: "image/jpeg", source: nil, password: nil)
|
|
26
|
+
file_data = read_file(file)
|
|
27
|
+
|
|
28
|
+
fields = {}
|
|
29
|
+
fields["source"] = source if source
|
|
30
|
+
|
|
31
|
+
headers = {}
|
|
32
|
+
headers["x-session-password"] = password if password
|
|
33
|
+
|
|
34
|
+
@http.request_multipart(
|
|
35
|
+
"/api/v1/upload/#{uuid}/images",
|
|
36
|
+
file_data,
|
|
37
|
+
filename: filename,
|
|
38
|
+
mime_type: mime_type,
|
|
39
|
+
fields: fields,
|
|
40
|
+
headers: headers
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Upload an encrypted image to a session.
|
|
45
|
+
#
|
|
46
|
+
# The image is encrypted client-side using AES-256-GCM, with the AES key
|
|
47
|
+
# wrapped by the server's RSA public key. The encrypted payload is sent as
|
|
48
|
+
# JSON with the +X-Aptr-Encrypted: default+ header.
|
|
49
|
+
#
|
|
50
|
+
# @param uuid [String] the session UUID
|
|
51
|
+
# @param file [String, IO] a file path, IO, or raw bytes
|
|
52
|
+
# @param public_key [String] the RSA public key in PEM format
|
|
53
|
+
# @param filename [String] the filename (default: "image.jpg")
|
|
54
|
+
# @param mime_type [String] the MIME type (default: "image/jpeg")
|
|
55
|
+
# @param source [String, nil] optional source identifier
|
|
56
|
+
# @param password [String, nil] session password if the session is protected
|
|
57
|
+
# @return [Hash] upload result
|
|
58
|
+
def image_encrypted(uuid, file, public_key, filename: "image.jpg", mime_type: "image/jpeg", source: nil, password: nil)
|
|
59
|
+
file_data = read_file(file)
|
|
60
|
+
encrypted = Apertur::Crypto.encrypt_image(file_data, public_key)
|
|
61
|
+
|
|
62
|
+
payload = encrypted.merge(
|
|
63
|
+
"filename" => filename,
|
|
64
|
+
"mimeType" => mime_type,
|
|
65
|
+
"source" => source || "sdk"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
headers = {
|
|
69
|
+
"X-Aptr-Encrypted" => "default"
|
|
70
|
+
}
|
|
71
|
+
headers["x-session-password"] = password if password
|
|
72
|
+
|
|
73
|
+
@http.request(:post, "/api/v1/upload/#{uuid}/images", body: payload, headers: headers)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
# Normalize file input to raw bytes.
|
|
79
|
+
#
|
|
80
|
+
# @param file [String, IO] file path, IO object, or raw bytes
|
|
81
|
+
# @return [String] raw binary data
|
|
82
|
+
def read_file(file)
|
|
83
|
+
if file.respond_to?(:read)
|
|
84
|
+
file.read.b
|
|
85
|
+
elsif file.is_a?(String) && File.exist?(file) && file.length < 1024
|
|
86
|
+
# Treat short strings that point to existing files as paths.
|
|
87
|
+
# Raw image bytes will almost never be < 1024 bytes AND match an
|
|
88
|
+
# existing filename, so this heuristic is safe in practice.
|
|
89
|
+
File.binread(file)
|
|
90
|
+
elsif file.is_a?(String)
|
|
91
|
+
file.b
|
|
92
|
+
else
|
|
93
|
+
raise ArgumentError, "Unsupported file input. Use a file path String, IO object, or raw String bytes."
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Browse completed uploads.
|
|
6
|
+
class Uploads
|
|
7
|
+
# @param http [Apertur::HttpClient]
|
|
8
|
+
def initialize(http)
|
|
9
|
+
@http = http
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# List uploads with pagination.
|
|
13
|
+
#
|
|
14
|
+
# @param page [Integer, nil] page number
|
|
15
|
+
# @param page_size [Integer, nil] number of results per page
|
|
16
|
+
# @return [Hash] paginated list with +data+ and pagination metadata
|
|
17
|
+
def list(**params)
|
|
18
|
+
query = {}
|
|
19
|
+
query["page"] = params[:page].to_s if params[:page]
|
|
20
|
+
query["pageSize"] = params[:page_size].to_s if params[:page_size]
|
|
21
|
+
@http.request(:get, "/api/v1/uploads", query: query)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# List recent uploads.
|
|
25
|
+
#
|
|
26
|
+
# @param limit [Integer, nil] maximum number of uploads to return
|
|
27
|
+
# @return [Array<Hash>] recent uploads
|
|
28
|
+
def recent(**params)
|
|
29
|
+
query = {}
|
|
30
|
+
query["limit"] = params[:limit].to_s if params[:limit]
|
|
31
|
+
@http.request(:get, "/api/v1/uploads/recent", query: query)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Apertur
|
|
4
|
+
module Resources
|
|
5
|
+
# Manage event webhooks within a project.
|
|
6
|
+
class Webhooks
|
|
7
|
+
# @param http [Apertur::HttpClient]
|
|
8
|
+
def initialize(http)
|
|
9
|
+
@http = http
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# List all webhooks for a project.
|
|
13
|
+
#
|
|
14
|
+
# @param project_id [String] the project ID
|
|
15
|
+
# @return [Array<Hash>] list of webhooks
|
|
16
|
+
def list(project_id)
|
|
17
|
+
@http.request(:get, "/api/v1/projects/#{project_id}/webhooks")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Create a new webhook.
|
|
21
|
+
#
|
|
22
|
+
# @param project_id [String] the project ID
|
|
23
|
+
# @param config [Hash] webhook configuration (e.g. +url+, +events+, +secret+)
|
|
24
|
+
# @return [Hash] the created webhook
|
|
25
|
+
def create(project_id, **config)
|
|
26
|
+
@http.request(:post, "/api/v1/projects/#{project_id}/webhooks", body: config)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Update an existing webhook.
|
|
30
|
+
#
|
|
31
|
+
# @param project_id [String] the project ID
|
|
32
|
+
# @param webhook_id [String] the webhook ID
|
|
33
|
+
# @param config [Hash] fields to update
|
|
34
|
+
# @return [Hash] the updated webhook
|
|
35
|
+
def update(project_id, webhook_id, **config)
|
|
36
|
+
@http.request(:patch, "/api/v1/projects/#{project_id}/webhooks/#{webhook_id}", body: config)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Delete a webhook.
|
|
40
|
+
#
|
|
41
|
+
# @param project_id [String] the project ID
|
|
42
|
+
# @param webhook_id [String] the webhook ID
|
|
43
|
+
# @return [nil]
|
|
44
|
+
def delete(project_id, webhook_id)
|
|
45
|
+
@http.request(:delete, "/api/v1/projects/#{project_id}/webhooks/#{webhook_id}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Send a test event to a webhook.
|
|
49
|
+
#
|
|
50
|
+
# @param project_id [String] the project ID
|
|
51
|
+
# @param webhook_id [String] the webhook ID
|
|
52
|
+
# @return [Hash] test result
|
|
53
|
+
def test(project_id, webhook_id)
|
|
54
|
+
@http.request(:post, "/api/v1/projects/#{project_id}/webhooks/#{webhook_id}/test")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# List delivery attempts for a webhook.
|
|
58
|
+
#
|
|
59
|
+
# @param project_id [String] the project ID
|
|
60
|
+
# @param webhook_id [String] the webhook ID
|
|
61
|
+
# @param page [Integer, nil] page number
|
|
62
|
+
# @param limit [Integer, nil] number of results per page
|
|
63
|
+
# @return [Hash] paginated delivery list
|
|
64
|
+
def deliveries(project_id, webhook_id, **options)
|
|
65
|
+
query = {}
|
|
66
|
+
query["page"] = options[:page].to_s if options[:page]
|
|
67
|
+
query["limit"] = options[:limit].to_s if options[:limit]
|
|
68
|
+
@http.request(:get, "/api/v1/projects/#{project_id}/webhooks/#{webhook_id}/deliveries", query: query)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Retry a failed delivery attempt.
|
|
72
|
+
#
|
|
73
|
+
# @param project_id [String] the project ID
|
|
74
|
+
# @param webhook_id [String] the webhook ID
|
|
75
|
+
# @param delivery_id [String] the delivery ID
|
|
76
|
+
# @return [Hash] retry result
|
|
77
|
+
def retry_delivery(project_id, webhook_id, delivery_id)
|
|
78
|
+
@http.request(
|
|
79
|
+
:post,
|
|
80
|
+
"/api/v1/projects/#{project_id}/webhooks/#{webhook_id}/deliveries/#{delivery_id}/retry"
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
|
|
5
|
+
module Apertur
|
|
6
|
+
# Webhook signature verification utilities.
|
|
7
|
+
#
|
|
8
|
+
# Provides constant-time signature verification for three webhook formats
|
|
9
|
+
# used by the Apertur platform.
|
|
10
|
+
module Signature
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# Verify an image delivery webhook signature.
|
|
14
|
+
#
|
|
15
|
+
# The signature header is formatted as +sha256=<hex>+ and is computed as
|
|
16
|
+
# +HMAC-SHA256(body, secret)+.
|
|
17
|
+
#
|
|
18
|
+
# @param body [String] the raw request body
|
|
19
|
+
# @param signature [String] the signature header value (e.g. "sha256=abc123...")
|
|
20
|
+
# @param secret [String] the webhook signing secret
|
|
21
|
+
# @return [Boolean] true if the signature is valid
|
|
22
|
+
def verify_webhook(body, signature, secret)
|
|
23
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, body)
|
|
24
|
+
sig = signature.start_with?("sha256=") ? signature[7..] : signature
|
|
25
|
+
secure_compare(expected, sig)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Verify an event webhook signature (HMAC SHA256 method).
|
|
29
|
+
#
|
|
30
|
+
# The signed payload is +"\#{timestamp}.\#{body}"+ and the signature header
|
|
31
|
+
# is formatted as +sha256=<hex>+.
|
|
32
|
+
#
|
|
33
|
+
# @param body [String] the raw request body
|
|
34
|
+
# @param timestamp [String] the X-Apertur-Timestamp header value
|
|
35
|
+
# @param signature [String] the X-Apertur-Signature header value
|
|
36
|
+
# @param secret [String] the webhook signing secret
|
|
37
|
+
# @return [Boolean] true if the signature is valid
|
|
38
|
+
def verify_event(body, timestamp, signature, secret)
|
|
39
|
+
signature_base = "#{timestamp}.#{body}"
|
|
40
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_base)
|
|
41
|
+
sig = signature.start_with?("sha256=") ? signature[7..] : signature
|
|
42
|
+
secure_compare(expected, sig)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Verify an event webhook signature (Svix method).
|
|
46
|
+
#
|
|
47
|
+
# The signed payload is +"\#{svix_id}.\#{timestamp}.\#{body}"+ and the
|
|
48
|
+
# signing key is the secret decoded from hex. The signature header is
|
|
49
|
+
# formatted as +v1,<base64>+.
|
|
50
|
+
#
|
|
51
|
+
# @param body [String] the raw request body
|
|
52
|
+
# @param svix_id [String] the svix-id header value
|
|
53
|
+
# @param timestamp [String] the svix-timestamp header value
|
|
54
|
+
# @param signature [String] the svix-signature header value (e.g. "v1,base64...")
|
|
55
|
+
# @param secret [String] the webhook signing secret (hex-encoded)
|
|
56
|
+
# @return [Boolean] true if the signature is valid
|
|
57
|
+
def verify_svix(body, svix_id, timestamp, signature, secret)
|
|
58
|
+
signature_base = "#{svix_id}.#{timestamp}.#{body}"
|
|
59
|
+
key = [secret].pack("H*")
|
|
60
|
+
expected = OpenSSL::HMAC.digest("SHA256", key, signature_base)
|
|
61
|
+
expected_b64 = [expected].pack("m0")
|
|
62
|
+
sig = signature.start_with?("v1,") ? signature[3..] : signature
|
|
63
|
+
secure_compare(expected_b64, sig)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Constant-time string comparison to prevent timing attacks.
|
|
67
|
+
#
|
|
68
|
+
# @param a [String]
|
|
69
|
+
# @param b [String]
|
|
70
|
+
# @return [Boolean]
|
|
71
|
+
def secure_compare(a, b)
|
|
72
|
+
return false unless a.bytesize == b.bytesize
|
|
73
|
+
|
|
74
|
+
OpenSSL.fixed_length_secure_compare(a, b)
|
|
75
|
+
rescue StandardError
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/apertur.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "apertur/version"
|
|
4
|
+
require_relative "apertur/errors"
|
|
5
|
+
require_relative "apertur/http_client"
|
|
6
|
+
require_relative "apertur/signature"
|
|
7
|
+
require_relative "apertur/crypto"
|
|
8
|
+
require_relative "apertur/resources/sessions"
|
|
9
|
+
require_relative "apertur/resources/upload"
|
|
10
|
+
require_relative "apertur/resources/uploads"
|
|
11
|
+
require_relative "apertur/resources/polling"
|
|
12
|
+
require_relative "apertur/resources/destinations"
|
|
13
|
+
require_relative "apertur/resources/keys"
|
|
14
|
+
require_relative "apertur/resources/webhooks"
|
|
15
|
+
require_relative "apertur/resources/encryption"
|
|
16
|
+
require_relative "apertur/resources/stats"
|
|
17
|
+
require_relative "apertur/client"
|
|
18
|
+
|
|
19
|
+
# Ruby SDK for the Apertur image upload API.
|
|
20
|
+
#
|
|
21
|
+
# @example Quick start
|
|
22
|
+
# require "apertur"
|
|
23
|
+
#
|
|
24
|
+
# client = Apertur::Client.new(api_key: "aptr_test_abc123")
|
|
25
|
+
# session = client.sessions.create(max_images: 5)
|
|
26
|
+
# client.upload.image(session["uuid"], "/path/to/photo.jpg")
|
|
27
|
+
#
|
|
28
|
+
# @example Verify a webhook signature
|
|
29
|
+
# Apertur::Signature.verify_webhook(request_body, signature_header, secret)
|
|
30
|
+
#
|
|
31
|
+
# @see Apertur::Client
|
|
32
|
+
# @see Apertur::Signature
|
|
33
|
+
# @see Apertur::Crypto
|
|
34
|
+
module Apertur; end
|
metadata
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: apertur-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Apertur
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-04-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Official Ruby client for the Apertur image upload and delivery API. Supports
|
|
14
|
+
session management, image uploads (including client-side encryption), polling, destinations,
|
|
15
|
+
webhooks, and more.
|
|
16
|
+
email:
|
|
17
|
+
- support@aptr.ca
|
|
18
|
+
executables: []
|
|
19
|
+
extensions: []
|
|
20
|
+
extra_rdoc_files: []
|
|
21
|
+
files:
|
|
22
|
+
- LICENSE
|
|
23
|
+
- README.md
|
|
24
|
+
- lib/apertur.rb
|
|
25
|
+
- lib/apertur/client.rb
|
|
26
|
+
- lib/apertur/crypto.rb
|
|
27
|
+
- lib/apertur/errors.rb
|
|
28
|
+
- lib/apertur/http_client.rb
|
|
29
|
+
- lib/apertur/resources/destinations.rb
|
|
30
|
+
- lib/apertur/resources/encryption.rb
|
|
31
|
+
- lib/apertur/resources/keys.rb
|
|
32
|
+
- lib/apertur/resources/polling.rb
|
|
33
|
+
- lib/apertur/resources/sessions.rb
|
|
34
|
+
- lib/apertur/resources/stats.rb
|
|
35
|
+
- lib/apertur/resources/upload.rb
|
|
36
|
+
- lib/apertur/resources/uploads.rb
|
|
37
|
+
- lib/apertur/resources/webhooks.rb
|
|
38
|
+
- lib/apertur/signature.rb
|
|
39
|
+
- lib/apertur/version.rb
|
|
40
|
+
homepage: https://github.com/Apertur-dev/apertur-ruby
|
|
41
|
+
licenses:
|
|
42
|
+
- MIT
|
|
43
|
+
metadata:
|
|
44
|
+
homepage_uri: https://github.com/Apertur-dev/apertur-ruby
|
|
45
|
+
source_code_uri: https://github.com/Apertur-dev/apertur-ruby
|
|
46
|
+
changelog_uri: https://github.com/Apertur-dev/apertur-ruby/blob/main/CHANGELOG.md
|
|
47
|
+
documentation_uri: https://docs.apertur.ca
|
|
48
|
+
rubygems_mfa_required: 'true'
|
|
49
|
+
post_install_message:
|
|
50
|
+
rdoc_options: []
|
|
51
|
+
require_paths:
|
|
52
|
+
- lib
|
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
54
|
+
requirements:
|
|
55
|
+
- - ">="
|
|
56
|
+
- !ruby/object:Gem::Version
|
|
57
|
+
version: '3.0'
|
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - ">="
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: '0'
|
|
63
|
+
requirements: []
|
|
64
|
+
rubygems_version: 3.3.5
|
|
65
|
+
signing_key:
|
|
66
|
+
specification_version: 4
|
|
67
|
+
summary: Ruby SDK for the Apertur API
|
|
68
|
+
test_files: []
|