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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 43690cd24afff7146d8e06a8fa97981242ac044b9f681c594101f648f3771016
4
+ data.tar.gz: e9e4f5fd4d7097ae37a482fe44acf6277ea5bfe47abc730363ad86e1e4719aa2
5
+ SHA512:
6
+ metadata.gz: 242ce180cfb7061f9e232bfafb3c469fd4c7bb47b0ddf194150d97a17f734f25b224ece23e3cff1ee21b15bdc6eb04572a65b8a0e1de740748f8194a0520a4fb
7
+ data.tar.gz: a672905ff810166a17076d8db8df271002489399b08ed56f9b156135f7550f2d1493bf8c78ee2c49f401fd519ce30c671d6af358485bb085c9b604d1b1238388
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Apertur
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # Apertur Ruby SDK
2
+
3
+ Official Ruby client for the [Apertur](https://apertur.ca) image upload and delivery API.
4
+
5
+ ## Installation
6
+
7
+ Add to your Gemfile:
8
+
9
+ ```ruby
10
+ gem "apertur-sdk"
11
+ ```
12
+
13
+ Or install directly:
14
+
15
+ ```sh
16
+ gem install apertur-sdk
17
+ ```
18
+
19
+ ## Quick Start
20
+
21
+ ```ruby
22
+ require "apertur"
23
+
24
+ client = Apertur::Client.new(api_key: "aptr_test_your_key_here")
25
+
26
+ # Create an upload session
27
+ session = client.sessions.create(max_images: 10)
28
+ puts session["uuid"]
29
+
30
+ # Upload an image
31
+ result = client.upload.image(session["uuid"], "/path/to/photo.jpg")
32
+
33
+ # Upload with encryption
34
+ server_key = client.encryption.get_server_key
35
+ client.upload.image_encrypted(
36
+ session["uuid"],
37
+ "/path/to/photo.jpg",
38
+ server_key["publicKey"]
39
+ )
40
+ ```
41
+
42
+ ## Authentication
43
+
44
+ The SDK accepts an API key (prefixed `aptr_` or `aptr_test_`) or an OAuth token. The environment is auto-detected from the key prefix:
45
+
46
+ - `aptr_test_*` keys target the sandbox at `https://sandbox.api.aptr.ca`
47
+ - `aptr_*` keys target production at `https://api.aptr.ca`
48
+
49
+ You can override the base URL:
50
+
51
+ ```ruby
52
+ client = Apertur::Client.new(api_key: "aptr_...", base_url: "http://localhost:3000")
53
+ ```
54
+
55
+ ## Resources
56
+
57
+ ### Sessions
58
+
59
+ ```ruby
60
+ client.sessions.create(max_images: 5, expires_in_hours: 24)
61
+ client.sessions.get("uuid")
62
+ client.sessions.update("uuid", max_images: 10)
63
+ client.sessions.list(page: 1, page_size: 20)
64
+ client.sessions.recent(limit: 5)
65
+ client.sessions.qr("uuid", format: "png", size: 300)
66
+ client.sessions.verify_password("uuid", "secret")
67
+ client.sessions.delivery_status("uuid")
68
+ ```
69
+
70
+ ### Upload
71
+
72
+ ```ruby
73
+ # Multipart upload from file path
74
+ client.upload.image("uuid", "/path/to/image.jpg")
75
+
76
+ # Upload from IO
77
+ File.open("photo.png", "rb") do |f|
78
+ client.upload.image("uuid", f, filename: "photo.png", mime_type: "image/png")
79
+ end
80
+
81
+ # Encrypted upload
82
+ client.upload.image_encrypted("uuid", "/path/to/image.jpg", public_key_pem)
83
+ ```
84
+
85
+ ### Uploads
86
+
87
+ ```ruby
88
+ client.uploads.list(page: 1, page_size: 20)
89
+ client.uploads.recent(limit: 10)
90
+ ```
91
+
92
+ ### Polling
93
+
94
+ ```ruby
95
+ # One-shot poll
96
+ result = client.polling.list("uuid")
97
+ data = client.polling.download("uuid", image_id)
98
+ client.polling.ack("uuid", image_id)
99
+
100
+ # Blocking loop
101
+ client.polling.poll_and_process("uuid", interval: 3) do |image, data|
102
+ File.binwrite("downloads/#{image['id']}.jpg", data)
103
+ end
104
+ ```
105
+
106
+ ### Destinations
107
+
108
+ ```ruby
109
+ client.destinations.list("project_id")
110
+ client.destinations.create("project_id", type: "s3", bucket: "my-bucket")
111
+ client.destinations.update("project_id", "dest_id", bucket: "other-bucket")
112
+ client.destinations.delete("project_id", "dest_id")
113
+ client.destinations.test("project_id", "dest_id")
114
+ ```
115
+
116
+ ### Keys
117
+
118
+ ```ruby
119
+ client.keys.list("project_id")
120
+ client.keys.create("project_id", name: "My Key")
121
+ client.keys.update("project_id", "key_id", name: "Renamed Key")
122
+ client.keys.delete("project_id", "key_id")
123
+ client.keys.set_destinations("key_id", ["dest_1", "dest_2"], long_polling_enabled: true)
124
+ ```
125
+
126
+ ### Webhooks
127
+
128
+ ```ruby
129
+ client.webhooks.list("project_id")
130
+ client.webhooks.create("project_id", url: "https://example.com/hook", events: ["upload.completed"])
131
+ client.webhooks.update("project_id", "webhook_id", url: "https://example.com/hook2")
132
+ client.webhooks.delete("project_id", "webhook_id")
133
+ client.webhooks.test("project_id", "webhook_id")
134
+ client.webhooks.deliveries("project_id", "webhook_id", page: 1, limit: 20)
135
+ client.webhooks.retry_delivery("project_id", "webhook_id", "delivery_id")
136
+ ```
137
+
138
+ ### Encryption
139
+
140
+ ```ruby
141
+ server_key = client.encryption.get_server_key
142
+ ```
143
+
144
+ ### Stats
145
+
146
+ ```ruby
147
+ stats = client.stats.get
148
+ ```
149
+
150
+ ## Webhook Signature Verification
151
+
152
+ ```ruby
153
+ # Simple webhook
154
+ Apertur::Signature.verify_webhook(request_body, signature_header, secret)
155
+
156
+ # Event webhook with timestamp
157
+ Apertur::Signature.verify_event(request_body, timestamp_header, signature_header, secret)
158
+
159
+ # Svix-style webhook
160
+ Apertur::Signature.verify_svix(request_body, svix_id, svix_timestamp, svix_signature, secret)
161
+ ```
162
+
163
+ ## Client-Side Encryption
164
+
165
+ ```ruby
166
+ encrypted = Apertur::Crypto.encrypt_image(raw_bytes, public_key_pem)
167
+ # => { "encrypted_key" => "...", "iv" => "...", "encrypted_data" => "...", "algorithm" => "RSA-OAEP+AES-256-GCM" }
168
+ ```
169
+
170
+ ## Error Handling
171
+
172
+ ```ruby
173
+ begin
174
+ client.sessions.get("nonexistent")
175
+ rescue Apertur::NotFoundError => e
176
+ puts "Not found: #{e.message}"
177
+ rescue Apertur::AuthenticationError => e
178
+ puts "Auth failed: #{e.message}"
179
+ rescue Apertur::RateLimitError => e
180
+ puts "Rate limited, retry after #{e.retry_after}s"
181
+ rescue Apertur::ValidationError => e
182
+ puts "Invalid request: #{e.message}"
183
+ rescue Apertur::Error => e
184
+ puts "API error #{e.status_code}: #{e.message}"
185
+ end
186
+ ```
187
+
188
+ ## Requirements
189
+
190
+ - Ruby >= 3.0
191
+ - No external dependencies (uses `net/http`, `json`, and `openssl` from stdlib)
192
+
193
+ ## License
194
+
195
+ MIT
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apertur
4
+ # Main client for the Apertur API.
5
+ #
6
+ # Provides access to all API resources through lazily initialized accessors.
7
+ # Automatically detects the environment (live vs. sandbox) from the API key
8
+ # prefix and selects the appropriate base URL.
9
+ #
10
+ # @example
11
+ # client = Apertur::Client.new(api_key: "aptr_test_abc123")
12
+ # session = client.sessions.create(max_images: 5)
13
+ # puts session["uuid"]
14
+ class Client
15
+ DEFAULT_BASE_URL = "https://api.aptr.ca"
16
+ SANDBOX_BASE_URL = "https://sandbox.api.aptr.ca"
17
+
18
+ # @return [String] the environment this client targets ("live" or "test")
19
+ attr_reader :env
20
+
21
+ # @return [Apertur::Resources::Sessions]
22
+ attr_reader :sessions
23
+
24
+ # @return [Apertur::Resources::Upload]
25
+ attr_reader :upload
26
+
27
+ # @return [Apertur::Resources::Uploads]
28
+ attr_reader :uploads
29
+
30
+ # @return [Apertur::Resources::Polling]
31
+ attr_reader :polling
32
+
33
+ # @return [Apertur::Resources::Destinations]
34
+ attr_reader :destinations
35
+
36
+ # @return [Apertur::Resources::Keys]
37
+ attr_reader :keys
38
+
39
+ # @return [Apertur::Resources::Webhooks]
40
+ attr_reader :webhooks
41
+
42
+ # @return [Apertur::Resources::Encryption]
43
+ attr_reader :encryption
44
+
45
+ # @return [Apertur::Resources::Stats]
46
+ attr_reader :stats
47
+
48
+ # Create a new Apertur API client.
49
+ #
50
+ # @param api_key [String, nil] an API key (prefixed +aptr_+ or +aptr_test_+)
51
+ # @param oauth_token [String, nil] an OAuth bearer token (alternative to api_key)
52
+ # @param base_url [String, nil] override the base URL; auto-detected from the
53
+ # key prefix when nil
54
+ # @raise [ArgumentError] if neither +api_key+ nor +oauth_token+ is provided
55
+ def initialize(api_key: nil, oauth_token: nil, base_url: nil)
56
+ token = api_key || oauth_token
57
+ raise ArgumentError, "Either api_key or oauth_token must be provided" if token.nil? || token.empty?
58
+
59
+ @env = token.start_with?("aptr_test_") ? "test" : "live"
60
+
61
+ resolved_url = base_url || (@env == "test" ? SANDBOX_BASE_URL : DEFAULT_BASE_URL)
62
+ http = HttpClient.new(resolved_url, token)
63
+
64
+ @sessions = Resources::Sessions.new(http)
65
+ @upload = Resources::Upload.new(http)
66
+ @uploads = Resources::Uploads.new(http)
67
+ @polling = Resources::Polling.new(http)
68
+ @destinations = Resources::Destinations.new(http)
69
+ @keys = Resources::Keys.new(http)
70
+ @webhooks = Resources::Webhooks.new(http)
71
+ @encryption = Resources::Encryption.new(http)
72
+ @stats = Resources::Stats.new(http)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "base64"
5
+ require "securerandom"
6
+
7
+ module Apertur
8
+ # Image encryption utilities for client-side encryption before upload.
9
+ #
10
+ # Uses AES-256-GCM for symmetric encryption and RSA-OAEP (SHA-256) to
11
+ # wrap the AES key with the server's public key.
12
+ module Crypto
13
+ module_function
14
+
15
+ # Encrypt image data for secure upload.
16
+ #
17
+ # Generates a random AES-256-GCM key and IV, encrypts the image data,
18
+ # then wraps the AES key with the provided RSA public key using OAEP
19
+ # padding with SHA-256.
20
+ #
21
+ # @param image_data [String] raw image bytes
22
+ # @param public_key_pem [String] RSA public key in PEM format
23
+ # @return [Hash] a Hash with the following String keys:
24
+ # - +"encrypted_key"+ - Base64-encoded RSA-wrapped AES key
25
+ # - +"iv"+ - Base64-encoded initialization vector
26
+ # - +"encrypted_data"+ - Base64-encoded ciphertext with appended GCM auth tag
27
+ # - +"algorithm"+ - the encryption algorithm identifier ("RSA-OAEP+AES-256-GCM")
28
+ def encrypt_image(image_data, public_key_pem)
29
+ # Generate random AES-256 key and 12-byte IV
30
+ aes_key = SecureRandom.random_bytes(32)
31
+ iv = SecureRandom.random_bytes(12)
32
+
33
+ # Encrypt image with AES-256-GCM
34
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
35
+ cipher.encrypt
36
+ cipher.key = aes_key
37
+ cipher.iv = iv
38
+
39
+ encrypted = cipher.update(image_data) + cipher.final
40
+ auth_tag = cipher.auth_tag
41
+ encrypted_with_tag = encrypted + auth_tag
42
+
43
+ # Wrap AES key with RSA-OAEP (SHA-256)
44
+ # Uses OpenSSL::PKey::PKey#encrypt (available in Ruby 3.0+) which
45
+ # allows specifying the OAEP digest algorithm.
46
+ pub_key = OpenSSL::PKey::RSA.new(public_key_pem)
47
+ wrapped_key = pub_key.encrypt(aes_key, {
48
+ "rsa_padding_mode" => "oaep",
49
+ "rsa_oaep_md" => "sha256"
50
+ })
51
+
52
+ {
53
+ "encrypted_key" => Base64.strict_encode64(wrapped_key),
54
+ "iv" => Base64.strict_encode64(iv),
55
+ "encrypted_data" => Base64.strict_encode64(encrypted_with_tag),
56
+ "algorithm" => "RSA-OAEP+AES-256-GCM"
57
+ }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apertur
4
+ # Base error class for all Apertur API errors.
5
+ #
6
+ # @attr_reader [Integer, nil] status_code the HTTP status code
7
+ # @attr_reader [String, nil] code the error code returned by the API
8
+ class Error < StandardError
9
+ attr_reader :status_code, :code
10
+
11
+ # @param message [String] the error message
12
+ # @param status_code [Integer, nil] the HTTP status code
13
+ # @param code [String, nil] the API error code
14
+ def initialize(message, status_code: nil, code: nil)
15
+ super(message)
16
+ @status_code = status_code
17
+ @code = code
18
+ end
19
+ end
20
+
21
+ # Raised when the API returns a 401 Unauthorized response.
22
+ class AuthenticationError < Error
23
+ def initialize(message = "Authentication failed")
24
+ super(message, status_code: 401, code: "AUTHENTICATION_FAILED")
25
+ end
26
+ end
27
+
28
+ # Raised when the API returns a 404 Not Found response.
29
+ class NotFoundError < Error
30
+ def initialize(message = "Not found")
31
+ super(message, status_code: 404, code: "NOT_FOUND")
32
+ end
33
+ end
34
+
35
+ # Raised when the API returns a 429 Too Many Requests response.
36
+ #
37
+ # @attr_reader [Integer, nil] retry_after seconds to wait before retrying
38
+ class RateLimitError < Error
39
+ attr_reader :retry_after
40
+
41
+ # @param message [String] the error message
42
+ # @param retry_after [Integer, nil] seconds to wait before retrying
43
+ def initialize(message = "Rate limit exceeded", retry_after: nil)
44
+ super(message, status_code: 429, code: "RATE_LIMIT")
45
+ @retry_after = retry_after
46
+ end
47
+ end
48
+
49
+ # Raised when the API returns a 400 Bad Request response.
50
+ class ValidationError < Error
51
+ def initialize(message = "Validation failed")
52
+ super(message, status_code: 400, code: "VALIDATION_ERROR")
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require "securerandom"
7
+
8
+ module Apertur
9
+ # Low-level HTTP wrapper around Net::HTTP for communicating with the Apertur API.
10
+ #
11
+ # Handles JSON serialization, Bearer token authentication, multipart uploads,
12
+ # and error mapping.
13
+ class HttpClient
14
+ # @param base_url [String] the API base URL (e.g. "https://api.aptr.ca")
15
+ # @param token [String] the Bearer token (API key or OAuth token)
16
+ def initialize(base_url, token)
17
+ @base_url = base_url.chomp("/")
18
+ @token = token
19
+ end
20
+
21
+ # Perform an API request and return the parsed JSON response.
22
+ #
23
+ # @param method [Symbol] HTTP method (:get, :post, :patch, :put, :delete)
24
+ # @param path [String] the API path (e.g. "/api/v1/stats")
25
+ # @param body [Hash, nil] request body to be serialized as JSON
26
+ # @param query [Hash, nil] query parameters
27
+ # @param headers [Hash] additional request headers
28
+ # @return [Hash, Array, nil] parsed JSON response, or nil for 204
29
+ # @raise [Apertur::Error] on API errors
30
+ def request(method, path, body: nil, query: nil, headers: {})
31
+ uri = build_uri(path, query)
32
+ req = build_request(method, uri, headers)
33
+
34
+ if body
35
+ req["Content-Type"] = "application/json"
36
+ req.body = body.is_a?(String) ? body : JSON.generate(body)
37
+ end
38
+
39
+ response = execute(uri, req)
40
+ handle_response(response)
41
+ end
42
+
43
+ # Perform an API request and return the raw response body as a binary String.
44
+ #
45
+ # @param method [Symbol] HTTP method
46
+ # @param path [String] the API path
47
+ # @param query [Hash, nil] query parameters
48
+ # @return [String] raw response body (binary)
49
+ # @raise [Apertur::Error] on API errors
50
+ def request_raw(method, path, query: nil)
51
+ uri = build_uri(path, query)
52
+ req = build_request(method, uri)
53
+
54
+ response = execute(uri, req)
55
+ handle_error(response) unless response.is_a?(Net::HTTPSuccess)
56
+ response.body
57
+ end
58
+
59
+ # Perform a multipart/form-data upload request.
60
+ #
61
+ # @param path [String] the API path
62
+ # @param file_data [String] raw file bytes
63
+ # @param filename [String] the filename to use in the multipart part
64
+ # @param mime_type [String] the MIME type of the file
65
+ # @param fields [Hash] additional form fields
66
+ # @param headers [Hash] additional request headers
67
+ # @return [Hash, Array, nil] parsed JSON response
68
+ # @raise [Apertur::Error] on API errors
69
+ def request_multipart(path, file_data, filename:, mime_type:, fields: {}, headers: {})
70
+ uri = build_uri(path)
71
+ boundary = "AperturRubySDK#{SecureRandom.hex(16)}"
72
+
73
+ body = build_multipart_body(boundary, file_data, filename, mime_type, fields)
74
+
75
+ req = build_request(:post, uri, headers)
76
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
77
+ req.body = body
78
+
79
+ response = execute(uri, req)
80
+ handle_response(response)
81
+ end
82
+
83
+ private
84
+
85
+ # @param path [String]
86
+ # @param query [Hash, nil]
87
+ # @return [URI]
88
+ def build_uri(path, query = nil)
89
+ url = "#{@base_url}#{path}"
90
+ if query && !query.empty?
91
+ params = query.reject { |_, v| v.nil? }.map { |k, v| "#{URI.encode_www_form_component(k)}=#{URI.encode_www_form_component(v)}" }
92
+ url += "?#{params.join("&")}" unless params.empty?
93
+ end
94
+ URI.parse(url)
95
+ end
96
+
97
+ # @param method [Symbol]
98
+ # @param uri [URI]
99
+ # @param extra_headers [Hash]
100
+ # @return [Net::HTTPRequest]
101
+ def build_request(method, uri, extra_headers = {})
102
+ klass = case method
103
+ when :get then Net::HTTP::Get
104
+ when :post then Net::HTTP::Post
105
+ when :patch then Net::HTTP::Patch
106
+ when :put then Net::HTTP::Put
107
+ when :delete then Net::HTTP::Delete
108
+ else raise ArgumentError, "Unsupported HTTP method: #{method}"
109
+ end
110
+
111
+ req = klass.new(uri)
112
+ req["Authorization"] = "Bearer #{@token}" if @token && !@token.empty?
113
+ req["User-Agent"] = "apertur-sdk-ruby/#{Apertur::VERSION}"
114
+ req["Accept"] = "application/json"
115
+
116
+ extra_headers.each { |k, v| req[k] = v }
117
+ req
118
+ end
119
+
120
+ # @param uri [URI]
121
+ # @param req [Net::HTTPRequest]
122
+ # @return [Net::HTTPResponse]
123
+ def execute(uri, req)
124
+ http = Net::HTTP.new(uri.host, uri.port)
125
+ http.use_ssl = (uri.scheme == "https")
126
+ http.open_timeout = 30
127
+ http.read_timeout = 60
128
+ http.request(req)
129
+ end
130
+
131
+ # @param response [Net::HTTPResponse]
132
+ # @return [Hash, Array, nil]
133
+ def handle_response(response)
134
+ handle_error(response) unless response.is_a?(Net::HTTPSuccess)
135
+
136
+ return nil if response.code == "204" || response.body.nil? || response.body.empty?
137
+
138
+ JSON.parse(response.body)
139
+ end
140
+
141
+ # @param response [Net::HTTPResponse]
142
+ # @raise [Apertur::Error]
143
+ def handle_error(response)
144
+ body = begin
145
+ JSON.parse(response.body)
146
+ rescue StandardError
147
+ {}
148
+ end
149
+
150
+ message = body["message"] || "HTTP #{response.code}"
151
+ code = body["code"]
152
+
153
+ case response.code.to_i
154
+ when 401
155
+ raise AuthenticationError, message
156
+ when 404
157
+ raise NotFoundError, message
158
+ when 429
159
+ retry_after = response["Retry-After"]&.to_i
160
+ raise RateLimitError.new(message, retry_after: retry_after)
161
+ when 400
162
+ raise ValidationError, message
163
+ else
164
+ raise Error.new(message, status_code: response.code.to_i, code: code)
165
+ end
166
+ end
167
+
168
+ # @param boundary [String]
169
+ # @param file_data [String]
170
+ # @param filename [String]
171
+ # @param mime_type [String]
172
+ # @param fields [Hash]
173
+ # @return [String]
174
+ def build_multipart_body(boundary, file_data, filename, mime_type, fields)
175
+ parts = []
176
+
177
+ fields.each do |key, value|
178
+ parts << "--#{boundary}\r\n" \
179
+ "Content-Disposition: form-data; name=\"#{key}\"\r\n" \
180
+ "\r\n" \
181
+ "#{value}\r\n"
182
+ end
183
+
184
+ parts << "--#{boundary}\r\n" \
185
+ "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n" \
186
+ "Content-Type: #{mime_type}\r\n" \
187
+ "\r\n"
188
+
189
+ body = parts.join.b
190
+ body << file_data.b
191
+ body << "\r\n--#{boundary}--\r\n".b
192
+ body
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apertur
4
+ module Resources
5
+ # Manage delivery destinations within a project.
6
+ #
7
+ # Destinations define where uploaded images are delivered (e.g. cloud
8
+ # storage buckets, webhooks, etc.).
9
+ class Destinations
10
+ # @param http [Apertur::HttpClient]
11
+ def initialize(http)
12
+ @http = http
13
+ end
14
+
15
+ # List all destinations for a project.
16
+ #
17
+ # @param project_id [String] the project ID
18
+ # @return [Array<Hash>] list of destinations
19
+ def list(project_id)
20
+ @http.request(:get, "/api/v1/projects/#{project_id}/destinations")
21
+ end
22
+
23
+ # Create a new destination.
24
+ #
25
+ # @param project_id [String] the project ID
26
+ # @param config [Hash] destination configuration
27
+ # @return [Hash] the created destination
28
+ def create(project_id, **config)
29
+ @http.request(:post, "/api/v1/projects/#{project_id}/destinations", body: config)
30
+ end
31
+
32
+ # Update an existing destination.
33
+ #
34
+ # @param project_id [String] the project ID
35
+ # @param dest_id [String] the destination ID
36
+ # @param config [Hash] fields to update
37
+ # @return [Hash] the updated destination
38
+ def update(project_id, dest_id, **config)
39
+ @http.request(:patch, "/api/v1/projects/#{project_id}/destinations/#{dest_id}", body: config)
40
+ end
41
+
42
+ # Delete a destination.
43
+ #
44
+ # @param project_id [String] the project ID
45
+ # @param dest_id [String] the destination ID
46
+ # @return [nil]
47
+ def delete(project_id, dest_id)
48
+ @http.request(:delete, "/api/v1/projects/#{project_id}/destinations/#{dest_id}")
49
+ end
50
+
51
+ # Send a test payload to a destination.
52
+ #
53
+ # @param project_id [String] the project ID
54
+ # @param dest_id [String] the destination ID
55
+ # @return [Hash] test result
56
+ def test(project_id, dest_id)
57
+ @http.request(:post, "/api/v1/projects/#{project_id}/destinations/#{dest_id}/test")
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Apertur
4
+ module Resources
5
+ # Retrieve server-side encryption keys.
6
+ class Encryption
7
+ # @param http [Apertur::HttpClient]
8
+ def initialize(http)
9
+ @http = http
10
+ end
11
+
12
+ # Get the server's public encryption key.
13
+ #
14
+ # The returned key is used for client-side image encryption before upload.
15
+ #
16
+ # @return [Hash] server key details including the PEM-encoded public key
17
+ def get_server_key
18
+ @http.request(:get, "/api/v1/encryption/server-key")
19
+ end
20
+ end
21
+ end
22
+ end