assinafy 1.5.1 → 1.5.2

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: ae7a039c9b9f51e0926dbd829524cd2f419896b16f1fdfce360ec6cabc967e8b
4
- data.tar.gz: de10da88b9213af201bc2e9fa12a72ef314fec4dd3ca67002c84d7a47fe08198
3
+ metadata.gz: 37dc8933996b9192a9f12896d772d4d5f745169fcaf0bb13b06cd199e4650bcd
4
+ data.tar.gz: 70fe0b92c876f387aeb3656f18b0e78d3cbc09d41dbefc73bba96bb6177b423b
5
5
  SHA512:
6
- metadata.gz: 12555ae0c37c4105795b3187261023fd90da12c5f2cd07f2d95f0512100ad1d2ffe4fa568a9c5166cc61ae0ee3a293c8297462c3cb7c77e8344cda517d76ba04
7
- data.tar.gz: 7e4f03a2f86ed9a0257de5585b01a4e08fa2e534d65ce9fb56b137c6ab14da1cdb237031d02e0f39061c5eefe0a7bb2d5057ca9f5cc64e36ddb3fa3cc053b3f9
6
+ metadata.gz: 5bd2d4cfc525e22c84d4de1cc232f4666b769bcdd66e21d56397ad1c4a61bc1c482a7a9b183dcadfde1bd098e624ada2776f847e2c55fcf65e43d20f89437d34
7
+ data.tar.gz: 4586c42fc917ef30c6e7eef7718310fd583ce2817acecdf2de5f3f972b405e8986c9a474aa5dbdd9f3c476b2651a700c641471c8154d710e85f2ba4c7c7ebbc1
data/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  All notable changes to the `assinafy` Ruby gem are documented here.
4
4
 
5
+ ## 1.5.2
6
+
7
+ ### Fixed
8
+
9
+ - `base_url` must now be an absolute `http`/`https` URL with a host. Other schemes, scheme-less
10
+ hosts, and relative paths raise `Assinafy::ValidationError` at construction instead of sending
11
+ credentials to them or surfacing a raw `URI::InvalidURIError`.
12
+ - `AssignmentResource.build_payload` now rejects a non-String `message` or `expires_at` and a
13
+ `copy_receivers` value that is not an array of non-empty signer IDs, instead of forwarding them
14
+ to the API verbatim.
15
+ - `Client#upload_and_request_signatures` validates the whole assignment body before uploading, so
16
+ malformed `message`, `expires_at`, or `copy_receivers` values no longer leave an uploaded
17
+ document and freshly created signers behind.
18
+
19
+ ### Changed
20
+
21
+ - Authentication and API-key operations route through the shared resource request helpers used by
22
+ every other resource.
23
+
5
24
  ## 1.5.1
6
25
 
7
26
  ### Added
data/README.md CHANGED
@@ -84,9 +84,12 @@ client = Assinafy::Client.new(
84
84
  - `token:` sends `Authorization: Bearer ...` (legacy session token).
85
85
  - Configure exactly one credential. If both are supplied, the SDK sends only `X-Api-Key`.
86
86
  - A client can also be created with no credentials for authentication and public/signer endpoints.
87
+ - `base_url:` must be an absolute `http`/`https` URL. Anything else — a scheme-less host, a relative path, or
88
+ another scheme — raises `Assinafy::ValidationError` instead of attaching your credentials to it. A trailing
89
+ slash is stripped.
87
90
  - Account-scoped methods document a per-call account override for multi-workspace tenants.
88
91
  - Provide a `Logger`-compatible `logger:` to observe upload/assignment/webhook lifecycle messages.
89
- - Requests send `User-Agent: Assinafy-Ruby-SDK/v1.5.1`; the suffix always follows `Assinafy::VERSION`.
92
+ - Requests send `User-Agent: Assinafy-Ruby-SDK/v<Assinafy::VERSION>`; the suffix always follows the gem version.
90
93
 
91
94
  `Client.from_config(hash)` accepts string- or symbol-keyed hashes (e.g. parsed YAML).
92
95
 
@@ -168,6 +168,23 @@ environment before making these operations part of a critical workflow.
168
168
  | DELETE | `/v1/accounts/{account_id}/templates/{template_id}` | `TemplateResource#delete` | Bearer token or `X-Api-Key` | No request body; returns `nil` on success. |
169
169
  | GET | `/v1/accounts/{account_id}/templates/{template_id}/pages/{page_id}/download` | `TemplateResource#download_page` | Bearer token or `X-Api-Key` | No request body; returns binary image bytes. |
170
170
 
171
+ ### Assignment listing
172
+
173
+ `GET /v1/assignments` needs an account context that the machine contract does not list. The SDK supplies it as
174
+ the camelCase `accountId` query parameter (`AssignmentResource#list`), taken from the client default or the
175
+ per-call override.
176
+
177
+ ### Assignment optional fields
178
+
179
+ `message` and `expires_at` must be strings, and `copy_receivers` must be an array of non-empty signer IDs.
180
+ `AssignmentResource.build_payload` rejects other shapes locally, so `Client#upload_and_request_signatures`
181
+ fails before it uploads a document or creates signers.
182
+
183
+ ### Base URL
184
+
185
+ `base_url` must be an absolute `http`/`https` URL with a host. Other schemes, scheme-less hosts, and relative
186
+ paths raise `Assinafy::ValidationError` at construction rather than sending credentials to them.
187
+
171
188
  ### Document tags
172
189
 
173
190
  `DocumentResource#replace_tags` and `#append_tags` accept arrays of tag IDs. The deployed sandbox also accepts
@@ -193,6 +193,13 @@ module Assinafy
193
193
  copy_receivers: nil, account_id: nil)
194
194
  validate_signature_workflow!(signers, wait_for_ready)
195
195
 
196
+ # Placeholder IDs stand in for the signers created below, so the whole
197
+ # assignment body is validated before anything is uploaded or created.
198
+ assignment_payload = { method: 'virtual', signers: Array.new(signers.length, 'preflight'),
199
+ message: message, expires_at: expires_at,
200
+ copy_receivers: copy_receivers }
201
+ Resources::AssignmentResource.build_payload(assignment_payload)
202
+
196
203
  @logger.info("Starting upload and signature workflow for #{signers.length} signer(s)")
197
204
 
198
205
  upload_opts = account_id.nil? ? {} : { account_id: account_id }
@@ -205,10 +212,7 @@ module Assinafy
205
212
  signer_ids << signer_id!(@signers.create(signer, account_id))
206
213
  end
207
214
 
208
- assignment_payload = { method: 'virtual', signers: signer_ids,
209
- message: message, expires_at: expires_at,
210
- copy_receivers: copy_receivers }
211
- assignment = @assignments.create(document['id'], assignment_payload)
215
+ assignment = @assignments.create(document['id'], assignment_payload.merge(signers: signer_ids))
212
216
  created_resource_id!(assignment, 'Assignment')
213
217
 
214
218
  @logger.info("Upload and signature workflow completed for document #{document['id']}")
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'uri'
4
+
3
5
  require_relative 'errors'
4
6
 
5
7
  module Assinafy
@@ -24,6 +26,9 @@ module Assinafy
24
26
  DEFAULT_BASE_URL = 'https://api.assinafy.com.br/v1'
25
27
  # Default Faraday open/read timeout, in seconds.
26
28
  DEFAULT_TIMEOUT = 30
29
+ # Schemes accepted for {#base_url}. Credentials are attached to every request
30
+ # sent to this host, so anything that is not absolute HTTP(S) is rejected.
31
+ BASE_URL_SCHEMES = %w[http https].freeze
27
32
 
28
33
  # @!attribute [rw] api_key
29
34
  # @return [String, nil] sent as `X-Api-Key`
@@ -65,17 +70,17 @@ module Assinafy
65
70
  # @example Trailing slash on base_url is stripped
66
71
  # Assinafy::Configuration.new(base_url: 'https://api.assinafy.com.br/v1/').base_url
67
72
  # # => "https://api.assinafy.com.br/v1"
73
+ #
74
+ # @example A base_url that is not an absolute http(s) URL is rejected
75
+ # Assinafy::Configuration.new(base_url: 'api.assinafy.com.br/v1')
76
+ # # raises Assinafy::ValidationError ("Base URL must be an absolute http(s) URL")
68
77
  def initialize(api_key: nil, token: nil, account_id: nil,
69
78
  base_url: DEFAULT_BASE_URL, webhook_secret: nil,
70
79
  timeout: DEFAULT_TIMEOUT, logger: nil)
71
80
  @api_key = api_key
72
81
  @token = token
73
82
  @account_id = account_id
74
- raise ValidationError.new('Base URL is required') unless base_url.is_a?(String)
75
-
76
- @base_url = base_url.strip.sub(%r{/+\z}, '')
77
- raise ValidationError.new('Base URL is required') if @base_url.empty?
78
-
83
+ @base_url = normalize_base_url(base_url)
79
84
  @webhook_secret = webhook_secret
80
85
  @timeout = normalize_timeout(timeout)
81
86
  @logger = logger
@@ -146,6 +151,25 @@ module Assinafy
146
151
 
147
152
  private
148
153
 
154
+ # Strip the trailing slash and require an absolute http(s) URL with a host.
155
+ # Without this the SDK would happily attach `X-Api-Key` to an `ftp:` or
156
+ # scheme-less base, or surface a raw URI::InvalidURIError from Faraday.
157
+ def normalize_base_url(value)
158
+ raise ValidationError.new('Base URL is required') unless value.is_a?(String)
159
+
160
+ url = value.strip.sub(%r{/+\z}, '')
161
+ raise ValidationError.new('Base URL is required') if url.empty?
162
+
163
+ uri = URI.parse(url)
164
+ unless BASE_URL_SCHEMES.include?(uri.scheme) && !uri.host.to_s.empty?
165
+ raise ValidationError.new('Base URL must be an absolute http(s) URL', { base_url: value })
166
+ end
167
+
168
+ url
169
+ rescue URI::InvalidURIError => e
170
+ raise ValidationError.new("Base URL is not a valid URL: #{e.message}", { base_url: value })
171
+ end
172
+
149
173
  def normalize_timeout(value)
150
174
  raw = value.nil? ? DEFAULT_TIMEOUT : value
151
175
  seconds = Integer(raw, exception: false) if raw.is_a?(Integer) || raw.is_a?(String)
@@ -79,6 +79,7 @@ module Assinafy
79
79
  method = (p[:method] || 'virtual').to_s
80
80
 
81
81
  validate_method!(method, signers, entries, p)
82
+ validate_optional_fields!(p)
82
83
 
83
84
  result = { method: method }
84
85
  result[:signers] = signers.map { |ref| normalise_signer_ref(ref, options) } unless signers.empty?
@@ -106,6 +107,28 @@ module Assinafy
106
107
  raise ValidationError.new('entries are required for collect assignments')
107
108
  end
108
109
 
110
+ # `message`/`expires_at`/`copy_receivers` are forwarded verbatim, so a
111
+ # wrong type here would only fail server-side — after the caller has
112
+ # already uploaded a document and created signers.
113
+ def validate_optional_fields!(payload)
114
+ %i[message expires_at].each do |key|
115
+ value = payload[key]
116
+ next if value.nil? || value.is_a?(String)
117
+
118
+ raise ValidationError.new("#{key} must be a String", { key => value })
119
+ end
120
+
121
+ receivers = payload[:copy_receivers]
122
+ return if receivers.nil?
123
+
124
+ unless receivers.is_a?(Array) && receivers.all? { |id| id.is_a?(String) && !id.strip.empty? }
125
+ raise ValidationError.new(
126
+ 'copy_receivers must be an Array of signer IDs',
127
+ { copy_receivers: receivers }
128
+ )
129
+ end
130
+ end
131
+
109
132
  def extract_signer_refs(payload)
110
133
  return payload[:signers] if payload[:signers].is_a?(Array) && !payload[:signers].empty?
111
134
 
@@ -98,7 +98,7 @@ module Assinafy
98
98
  # # => nil
99
99
  def link_social_login(provider:, token:)
100
100
  call('Failed to link social login') do
101
- @connection.post('auth/link-social-login', body_params(provider: provider, token: token))
101
+ http_post('auth/link-social-login', body_params(provider: provider, token: token))
102
102
  end
103
103
  end
104
104
 
@@ -122,7 +122,7 @@ module Assinafy
122
122
  # # { "api_key" => "api-key-created-once" }
123
123
  def create_api_key(password:)
124
124
  call('Failed to create API key') do
125
- @connection.post('users/api-keys', body_params(password: password))
125
+ http_post('users/api-keys', body_params(password: password))
126
126
  end
127
127
  end
128
128
 
@@ -146,7 +146,7 @@ module Assinafy
146
146
  # resource.get_api_key # => nil
147
147
  def get_api_key
148
148
  call('Failed to get API key') do
149
- @connection.get('users/api-keys')
149
+ http_get('users/api-keys')
150
150
  end
151
151
  end
152
152
 
@@ -168,7 +168,7 @@ module Assinafy
168
168
  # # => nil
169
169
  def delete_api_key
170
170
  call_void('Failed to delete API key') do
171
- @connection.delete('users/api-keys')
171
+ http_delete('users/api-keys')
172
172
  end
173
173
  end
174
174
 
@@ -192,7 +192,7 @@ module Assinafy
192
192
  # # { "email" => "user@example.com" }
193
193
  def change_password(email:, password:, new_password:)
194
194
  call('Failed to change password') do
195
- @connection.put(
195
+ http_put(
196
196
  'authentication/change-password',
197
197
  body_params(email: email, password: password, new_password: new_password)
198
198
  )
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Assinafy
4
- VERSION = '1.5.1'
4
+ VERSION = '1.5.2'
5
5
  USER_AGENT = "Assinafy-Ruby-SDK/v#{VERSION}".freeze
6
6
  end
data/sig/assinafy.rbs CHANGED
@@ -68,6 +68,7 @@ module Assinafy
68
68
  class Configuration
69
69
  DEFAULT_BASE_URL: String
70
70
  DEFAULT_TIMEOUT: Integer
71
+ BASE_URL_SCHEMES: Array[String]
71
72
 
72
73
  attr_accessor api_key: String?
73
74
  attr_accessor token: String?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: assinafy
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.5.1
4
+ version: 1.5.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Assinafy SDK Contributors