apertur-sdk 0.1.6 → 0.1.8
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 +11 -0
- data/lib/apertur/client.rb +6 -2
- data/lib/apertur/http_client.rb +38 -2
- data/lib/apertur/signature.rb +25 -0
- data/lib/apertur/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 54295e7074aee323e8302c6024d025831ef1f37fd4fba2bd8663585be266759a
|
|
4
|
+
data.tar.gz: 14fc7f33d55226e794a53cb4608b7f4d3be6c2dc2af1ac94ed6cc889cbe8e7bd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9c7b1b326a2dc9adf6ba246af70486777b548c9ad95e357d8c7535d98abd88d82daf49da40a35013628160c58dc6d52ee5f8d889c433c5d8c85144eed31bd472
|
|
7
|
+
data.tar.gz: 25eccfae7934a7178cf95aacaddff4b195f15205e34d1cc0b18b05460754fb417c63bb7b11840d5a1f77a4b165ac67e4fcbbafb6c52c463fc0713e5182fa3a99
|
data/README.md
CHANGED
|
@@ -52,6 +52,17 @@ You can override the base URL:
|
|
|
52
52
|
client = Apertur::Client.new(api_key: "aptr_...", base_url: "http://localhost:3000")
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
### Request Signing (optional)
|
|
56
|
+
|
|
57
|
+
Pass `signing_secret:` to enable HMAC request signing. When set, every outgoing
|
|
58
|
+
JSON or no-body request is automatically signed, adding `X-Aptr-Signature` and
|
|
59
|
+
`X-Aptr-Timestamp` headers so the server can verify the request's authenticity.
|
|
60
|
+
Omit it for backwards-compatible, unsigned requests.
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
client = Apertur::Client.new(api_key: "aptr_...", signing_secret: "your_signing_secret")
|
|
64
|
+
```
|
|
65
|
+
|
|
55
66
|
## Resources
|
|
56
67
|
|
|
57
68
|
### Sessions
|
data/lib/apertur/client.rb
CHANGED
|
@@ -51,15 +51,19 @@ module Apertur
|
|
|
51
51
|
# @param oauth_token [String, nil] an OAuth bearer token (alternative to api_key)
|
|
52
52
|
# @param base_url [String, nil] override the base URL; auto-detected from the
|
|
53
53
|
# key prefix when nil
|
|
54
|
+
# @param signing_secret [String, nil] optional request signing secret. When
|
|
55
|
+
# present, every JSON request is automatically signed with
|
|
56
|
+
# +X-Aptr-Signature+ / +X-Aptr-Timestamp+ headers. Backwards-compatible:
|
|
57
|
+
# omit (or pass nil/empty) to send unsigned requests as before.
|
|
54
58
|
# @raise [ArgumentError] if neither +api_key+ nor +oauth_token+ is provided
|
|
55
|
-
def initialize(api_key: nil, oauth_token: nil, base_url: nil)
|
|
59
|
+
def initialize(api_key: nil, oauth_token: nil, base_url: nil, signing_secret: nil)
|
|
56
60
|
token = api_key || oauth_token
|
|
57
61
|
raise ArgumentError, "Either api_key or oauth_token must be provided" if token.nil? || token.empty?
|
|
58
62
|
|
|
59
63
|
@env = token.start_with?("aptr_test_") ? "test" : "live"
|
|
60
64
|
|
|
61
65
|
resolved_url = base_url || (@env == "test" ? SANDBOX_BASE_URL : DEFAULT_BASE_URL)
|
|
62
|
-
http = HttpClient.new(resolved_url, token)
|
|
66
|
+
http = HttpClient.new(resolved_url, token, signing_secret: signing_secret)
|
|
63
67
|
|
|
64
68
|
@sessions = Resources::Sessions.new(http)
|
|
65
69
|
@upload = Resources::Upload.new(http)
|
data/lib/apertur/http_client.rb
CHANGED
|
@@ -4,18 +4,24 @@ require "net/http"
|
|
|
4
4
|
require "uri"
|
|
5
5
|
require "json"
|
|
6
6
|
require "securerandom"
|
|
7
|
+
require_relative "signature"
|
|
7
8
|
|
|
8
9
|
module Apertur
|
|
9
10
|
# Low-level HTTP wrapper around Net::HTTP for communicating with the Apertur API.
|
|
10
11
|
#
|
|
11
12
|
# Handles JSON serialization, Bearer token authentication, multipart uploads,
|
|
12
|
-
# and error mapping.
|
|
13
|
+
# request signing, and error mapping.
|
|
13
14
|
class HttpClient
|
|
14
15
|
# @param base_url [String] the API base URL (e.g. "https://api.aptr.ca")
|
|
15
16
|
# @param token [String] the Bearer token (API key or OAuth token)
|
|
16
|
-
|
|
17
|
+
# @param signing_secret [String, nil] optional request signing secret. When
|
|
18
|
+
# present (non-nil, non-empty), every JSON request is automatically
|
|
19
|
+
# signed with +X-Aptr-Signature+ / +X-Aptr-Timestamp+ headers. Absent by
|
|
20
|
+
# default for backwards compatibility.
|
|
21
|
+
def initialize(base_url, token, signing_secret: nil)
|
|
17
22
|
@base_url = base_url.chomp("/")
|
|
18
23
|
@token = token
|
|
24
|
+
@signing_secret = signing_secret
|
|
19
25
|
end
|
|
20
26
|
|
|
21
27
|
# Perform an API request and return the parsed JSON response.
|
|
@@ -38,6 +44,11 @@ module Apertur
|
|
|
38
44
|
req.body = body.is_a?(String) ? body : JSON.generate(body)
|
|
39
45
|
end
|
|
40
46
|
|
|
47
|
+
# Sign the full request-target (path + query) the server receives as
|
|
48
|
+
# req.url — build_uri may have appended a query string, and the server
|
|
49
|
+
# signs the query too. uri.request_uri is exactly what goes on the wire.
|
|
50
|
+
sign_headers(method, uri.request_uri, req.body).each { |k, v| req[k] = v }
|
|
51
|
+
|
|
41
52
|
response = execute(uri, req, read_timeout: read_timeout)
|
|
42
53
|
handle_response(response)
|
|
43
54
|
end
|
|
@@ -53,6 +64,8 @@ module Apertur
|
|
|
53
64
|
uri = build_uri(path, query)
|
|
54
65
|
req = build_request(method, uri)
|
|
55
66
|
|
|
67
|
+
sign_headers(method, uri.request_uri, req.body).each { |k, v| req[k] = v }
|
|
68
|
+
|
|
56
69
|
response = execute(uri, req)
|
|
57
70
|
handle_error(response) unless response.is_a?(Net::HTTPSuccess)
|
|
58
71
|
response.body
|
|
@@ -68,6 +81,13 @@ module Apertur
|
|
|
68
81
|
# @param headers [Hash] additional request headers
|
|
69
82
|
# @return [Hash, Array, nil] parsed JSON response
|
|
70
83
|
# @raise [Apertur::Error] on API errors
|
|
84
|
+
#
|
|
85
|
+
# @note Multipart bodies are NOT signed. Net::HTTP streams the boundary
|
|
86
|
+
# and part framing after this point, so the exact bytes on the wire
|
|
87
|
+
# aren't available here to hash — signing an approximation would only
|
|
88
|
+
# produce a signature that fails server-side verification. Multipart
|
|
89
|
+
# uploads rely on Authorization (API key) auth instead; request signing
|
|
90
|
+
# covers the JSON request path only (mirrors the Node SDK).
|
|
71
91
|
def request_multipart(path, file_data, filename:, mime_type:, fields: {}, headers: {})
|
|
72
92
|
uri = build_uri(path)
|
|
73
93
|
boundary = "AperturRubySDK#{SecureRandom.hex(16)}"
|
|
@@ -169,6 +189,22 @@ module Apertur
|
|
|
169
189
|
end
|
|
170
190
|
end
|
|
171
191
|
|
|
192
|
+
# Compute +X-Aptr-Signature+ / +X-Aptr-Timestamp+ headers for a request, or
|
|
193
|
+
# +{}+ when no signing secret is configured.
|
|
194
|
+
#
|
|
195
|
+
# @param method [Symbol, String] the HTTP method
|
|
196
|
+
# @param path [String] the exact request path (no query string), verbatim
|
|
197
|
+
# @param body [String, nil] the exact bytes/string that will be sent on
|
|
198
|
+
# the wire as the request body (or +nil+ for none) — the server hashes
|
|
199
|
+
# whatever it actually receives, so an approximation here would just
|
|
200
|
+
# produce a signature that fails verification.
|
|
201
|
+
# @return [Hash{String=>String}]
|
|
202
|
+
def sign_headers(method, path, body)
|
|
203
|
+
return {} if @signing_secret.nil? || @signing_secret.empty?
|
|
204
|
+
|
|
205
|
+
Signature.sign_request(@signing_secret, method, path, body, Time.now.to_i)
|
|
206
|
+
end
|
|
207
|
+
|
|
172
208
|
# @param boundary [String]
|
|
173
209
|
# @param file_data [String]
|
|
174
210
|
# @param filename [String]
|
data/lib/apertur/signature.rb
CHANGED
|
@@ -63,6 +63,31 @@ module Apertur
|
|
|
63
63
|
secure_compare(expected_b64, sig)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
# Sign an outgoing API request (HMAC SHA256 method).
|
|
67
|
+
#
|
|
68
|
+
# The signed payload is
|
|
69
|
+
# +"\#{timestamp}.\#{method.upcase}.\#{path}.\#{sha256hex(body)}"+ and the
|
|
70
|
+
# signature header is formatted as +sha256=<hex>+.
|
|
71
|
+
#
|
|
72
|
+
# @param secret [String] the request signing secret
|
|
73
|
+
# @param method [String, Symbol] the HTTP method (case-insensitive; uppercased)
|
|
74
|
+
# @param path [String] the exact request path, verbatim (for apertur this
|
|
75
|
+
# includes the +/api/v1+ prefix, with no query string)
|
|
76
|
+
# @param body [String, nil] the exact serialized bytes/string sent as the
|
|
77
|
+
# request body; +nil+ hashes as the empty string
|
|
78
|
+
# @param timestamp [Integer] unix seconds
|
|
79
|
+
# @return [Hash{String=>String}] +{ "X-Aptr-Signature" => "sha256=<hex>",
|
|
80
|
+
# "X-Aptr-Timestamp" => "<unix seconds>" }+
|
|
81
|
+
def sign_request(secret, method, path, body, timestamp)
|
|
82
|
+
body_hash = OpenSSL::Digest::SHA256.hexdigest(body || "")
|
|
83
|
+
signature_base = "#{timestamp}.#{method.to_s.upcase}.#{path}.#{body_hash}"
|
|
84
|
+
signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_base)
|
|
85
|
+
{
|
|
86
|
+
"X-Aptr-Signature" => "sha256=#{signature}",
|
|
87
|
+
"X-Aptr-Timestamp" => timestamp.to_s
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
66
91
|
# Constant-time string comparison to prevent timing attacks.
|
|
67
92
|
#
|
|
68
93
|
# @param a [String]
|
data/lib/apertur/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: apertur-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Apertur
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-08-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Official Ruby client for the Apertur image upload and delivery API. Supports
|
|
14
14
|
session management, image uploads (including client-side encryption), polling, destinations,
|