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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eefd130f1dc0dfc251471d88510fb28d2464ddfe23b8a3c5055404708d1118a4
4
- data.tar.gz: e33c04ea6e50216fe7988d2d5fd05e0327de309bbd26d6956098a86ef07afa4e
3
+ metadata.gz: 54295e7074aee323e8302c6024d025831ef1f37fd4fba2bd8663585be266759a
4
+ data.tar.gz: 14fc7f33d55226e794a53cb4608b7f4d3be6c2dc2af1ac94ed6cc889cbe8e7bd
5
5
  SHA512:
6
- metadata.gz: '08c21fe268c9ddf93ddd2aa5d44e7a0312051287b272b639a7b131212384ef91c7020f13ae7ac86a7818ce6372a6e7a0fccbf8247df0102b4e5d20ae3f341685'
7
- data.tar.gz: 62d086c9c4ab7c9122831fc579e5224764ed892874df7a686ed603ad7eeed2a7c8f50c2c1d41511c72c5922eb243ddd2345b140c9f1f32219a34b7eade389ed1
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
@@ -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)
@@ -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
- def initialize(base_url, token)
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]
@@ -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]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Apertur
4
- VERSION = "0.1.6"
4
+ VERSION = "0.1.8"
5
5
  end
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.6
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-06-14 00:00:00.000000000 Z
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,