clicksign-ruby-sdk 0.1.4 → 0.1.5

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: e2fa45c00d6994b4009e9888eb982da569f1657c692b7e83a5db76c69067ebc2
4
- data.tar.gz: e74c86dab5a02752fd9bb3a388cf54c78ae90e9a376cdac163bf5dca090fd5e9
3
+ metadata.gz: a45728b201a9703e1d70cb6de98056f85da9a524d9d0c122a1ed951d6f98ef5b
4
+ data.tar.gz: 7a2aea99029254b4849b27090f40927d4cbf66d44cb516f121ea14c65544f141
5
5
  SHA512:
6
- metadata.gz: 86b4b41f6607d89ea58786225502c699902523d05ff72f60d983449fc9a318d794ab1c164ce1d475a073ea868d22ee65da53f7e36c61cb477e28e4cea7fa86f4
7
- data.tar.gz: 484d16275cda5bf7e85413dcc3383776988574de052d6715a7d08e591d54701b13e3dd52a69c9a000977f8d51cc07a7590968050c6456b35cec48c914086a737
6
+ metadata.gz: e9cb3bdaff221f8a8e58a9ef4ca47363962e19b7dcb4f22eb129cb1f5f3b60812550db76301ee8a36473be11490293932895d83c91ef60d10928206d090cd162
7
+ data.tar.gz: 8c3cd41e15ba5c21136fc3ee4b53fcb96c231620735c17401acbd046a032262b31a9a1617f07ec704e9a9593dedb8023dd82019afe156cee782356c78c297199
data/README.md CHANGED
@@ -95,6 +95,8 @@ A API usa o header `Authorization: <seu-token>` **sem** o prefixo `Bearer`.
95
95
 
96
96
  > **Segurança:** não commite tokens no código. Use variáveis de ambiente ou cofre de secrets (Rails credentials, etc.).
97
97
 
98
+ > **`api_key` é obrigatório em runtime:** o SDK não valida a presença do token no boot. Se `api_key` for `nil`, nenhum erro é levantado no `configure` — o `AuthenticationError` só aparece na primeira request HTTP. Use `ENV.fetch('CLICKSIGN_API_KEY')` (em vez de `ENV[]`) para detectar a ausência da variável no startup da aplicação.
99
+
98
100
  > **Multi-conta / multi-tenant:** se cada requisição pode usar credenciais diferentes (SaaS, workers por cliente), prefira [`Clicksign::Services`](#multi-conta-e-cliente-instantiável) em vez da config global.
99
101
 
100
102
  Para testar interativamente no console da gem:
@@ -188,7 +190,7 @@ Com `max_retries > 0`, o client reexecuta a requisição em erros **transitório
188
190
  - `Clicksign::RateLimitError`
189
191
  - `Clicksign::ServerError` (5xx)
190
192
 
191
- Backoff exponencial com **full jitter** (espera aleatória entre `0` e o teto da tentativa: `0,5s`, `1s`, `2s`… até **30s**), para evitar thundering herd quando muitos clientes falham ao mesmo tempo. Após esgotar as retentativas, a exceção original é relançada.
193
+ Backoff exponencial com **full jitter**: espera aleatória uniforme em `[0, teto)` onde o teto cresce como `0.5s × 2^(tentativa-1)` (0,5s 1s 2s…) com cap de **30s**. O zero é possível o jitter distribui a espera para evitar thundering herd. Após esgotar as retentativas, a exceção original é relançada.
192
194
 
193
195
  ```ruby
194
196
  Clicksign.configure do |c|
@@ -462,12 +464,33 @@ Envelope.list_events(envelope.id)
462
464
  # Eventos de um documento
463
465
  Document.list_events(document.id, envelope_id: envelope.id)
464
466
 
465
- # Criar evento customizado no documento
466
- Event.create_for_document(
467
+ # Criar evento de imagem no documento (comprovante JPEG)
468
+ Event.create_add_image(
469
+ envelope_id: envelope.id,
470
+ document_id: document.id,
471
+ title: 'Comprovante de identidade',
472
+ occurred_at: Time.now.iso8601,
473
+ content_base64: 'data:image/jpeg;base64,...'
474
+ )
475
+
476
+ # Criar evento customizado — token_email ou token_sms
477
+ Event.create_custom(
478
+ envelope_id: envelope.id,
479
+ document_id: document.id,
480
+ kind: 'token_email', # ou 'token_sms'
481
+ occurred_at: Time.now.iso8601,
482
+ signer_name: 'Maria Silva',
483
+ signer_email: 'maria@empresa.com',
484
+ # signer_phone_number: '11988887777' # obrigatório quando kind: 'token_sms'
485
+ )
486
+
487
+ # API de baixo nível — qualquer name customizado
488
+ Event.create(
467
489
  envelope_id: envelope.id,
468
490
  document_id: document.id,
469
491
  name: 'custom',
470
- data: { description: 'Etapa interna concluída' }
492
+ data: { kind: 'token_email', signer_name: 'Maria Silva',
493
+ signer_email: 'maria@empresa.com', occurred_at: Time.now.iso8601 }
471
494
  )
472
495
  ```
473
496
 
@@ -14,7 +14,7 @@ module Clicksign
14
14
  }.freeze
15
15
 
16
16
  def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
17
- write_timeout: 10, max_retries: 0)
17
+ write_timeout: 10, max_retries: 0)
18
18
  @api_key = api_key
19
19
  @base_url = base_url
20
20
  @open_timeout = open_timeout
@@ -88,11 +88,11 @@ module Clicksign
88
88
 
89
89
  def http_request(request, uri)
90
90
  Net::HTTP.start(uri.host, uri.port,
91
- use_ssl: uri.scheme == 'https',
92
- open_timeout: @open_timeout,
93
- read_timeout: @read_timeout,
94
- write_timeout: @write_timeout,
95
- &proc { |http| http.request(request) })
91
+ use_ssl: uri.scheme == 'https',
92
+ open_timeout: @open_timeout,
93
+ read_timeout: @read_timeout,
94
+ write_timeout: @write_timeout,
95
+ &proc { |http| http.request(request) })
96
96
  end
97
97
 
98
98
  def handle_response(response, context, start)
@@ -8,7 +8,7 @@ module Clicksign
8
8
  }.freeze
9
9
 
10
10
  attr_accessor :api_key, :base_url, :open_timeout, :read_timeout,
11
- :write_timeout, :max_retries, :logger
11
+ :write_timeout, :max_retries, :logger
12
12
 
13
13
  def initialize
14
14
  @base_url = 'https://app.clicksign.com/api/v3'
@@ -21,7 +21,7 @@ module Clicksign
21
21
  def environment=(env)
22
22
  url = ENVIRONMENTS.fetch(env.to_sym) do
23
23
  raise ArgumentError,
24
- "Unknown environment: #{env}. Valid: #{ENVIRONMENTS.keys.join(', ')}"
24
+ "Unknown environment: #{env}. Valid: #{ENVIRONMENTS.keys.join(', ')}"
25
25
  end
26
26
  self.base_url = url
27
27
  end
@@ -50,7 +50,8 @@ module Clicksign
50
50
  errors = body['errors']
51
51
  return response.message unless errors.is_a?(Array)
52
52
 
53
- errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
53
+ result = errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
54
+ result.empty? ? response.message : result
54
55
  end
55
56
  end
56
57
  end
@@ -5,7 +5,7 @@ module Clicksign
5
5
  attr_reader :status_code, :request_id, :response_body, :response_headers
6
6
 
7
7
  def initialize(message = nil, status_code: nil, request_id: nil,
8
- response_body: nil, response_headers: {})
8
+ response_body: nil, response_headers: {})
9
9
  super(message)
10
10
  @status_code = status_code
11
11
  @request_id = request_id
@@ -5,6 +5,7 @@ module Clicksign
5
5
  EVENTS = %i[request retry error].freeze
6
6
 
7
7
  @callbacks = Hash.new { |h, k| h[k] = [] }
8
+ @mutex = Mutex.new
8
9
 
9
10
  class << self
10
11
  def on(event, &block)
@@ -12,11 +13,12 @@ module Clicksign
12
13
  raise ArgumentError, "Unknown event: #{event}. Valid: #{EVENTS.join(', ')}"
13
14
  end
14
15
 
15
- @callbacks[event] << block
16
+ @mutex.synchronize { @callbacks[event] << block }
16
17
  end
17
18
 
18
19
  def publish(event, payload)
19
- @callbacks[event].each do |cb|
20
+ callbacks = @mutex.synchronize { @callbacks[event].dup }
21
+ callbacks.each do |cb|
20
22
  cb.call(payload)
21
23
  rescue StandardError => e
22
24
  Clicksign.configuration.logger&.warn(
@@ -28,7 +30,7 @@ module Clicksign
28
30
 
29
31
  # Removes all registered callbacks — intended for test teardown.
30
32
  def clear
31
- @callbacks = Hash.new { |h, k| h[k] = [] }
33
+ @mutex.synchronize { @callbacks = Hash.new { |h, k| h[k] = [] } }
32
34
  end
33
35
  end
34
36
  end
@@ -15,7 +15,7 @@ module Clicksign
15
15
  }.freeze
16
16
 
17
17
  def initialize(api_key:, base_url:, open_timeout: 2, read_timeout: 10,
18
- write_timeout: 10, max_retries: 0)
18
+ write_timeout: 10, max_retries: 0)
19
19
  @api_key = api_key
20
20
  @base_url = base_url
21
21
  @open_timeout = open_timeout
@@ -78,11 +78,11 @@ module Clicksign
78
78
 
79
79
  def http_post(request, uri)
80
80
  Net::HTTP.start(uri.host, uri.port,
81
- use_ssl: uri.scheme == 'https',
82
- open_timeout: @open_timeout,
83
- read_timeout: @read_timeout,
84
- write_timeout: @write_timeout,
85
- &proc { |http| http.request(request) })
81
+ use_ssl: uri.scheme == 'https',
82
+ open_timeout: @open_timeout,
83
+ read_timeout: @read_timeout,
84
+ write_timeout: @write_timeout,
85
+ &proc { |http| http.request(request) })
86
86
  end
87
87
 
88
88
  def headers
@@ -97,8 +97,8 @@ module Clicksign
97
97
  return nil if response.body.nil? || response.body.empty?
98
98
 
99
99
  JSON.parse(response.body)
100
- rescue JSON::ParserError
101
- nil
100
+ rescue JSON::ParserError => e
101
+ raise Error, "Invalid JSON response from bulk operations endpoint: #{e.message}"
102
102
  end
103
103
  end
104
104
  end
@@ -33,7 +33,7 @@ module Clicksign
33
33
  end
34
34
 
35
35
  def add_rubricate(signer_id:, document_id:, pages: nil, rubric_field: nil,
36
- kind: nil)
36
+ kind: nil)
37
37
  validate_ids!(signer_id, document_id)
38
38
  if pages.nil? && rubric_field.nil?
39
39
  raise ArgumentError, 'pages or rubric_field is required'
@@ -13,7 +13,8 @@ module Clicksign
13
13
  end
14
14
 
15
15
  def include(*types)
16
- @params['include'] = types.join(',')
16
+ existing = @params['include']&.split(',') || []
17
+ @params['include'] = (existing + types.map(&:to_s)).uniq.join(',')
17
18
  self
18
19
  end
19
20
 
@@ -27,13 +27,13 @@ module Clicksign
27
27
 
28
28
  def publish_retry(request, uri, attempt, error, delay)
29
29
  Instrumentation.publish(:retry, {
30
- method: request.method.downcase.to_sym,
31
- path: resource_path(uri),
32
- attempt: attempt,
33
- max_retries: @max_retries,
34
- error: error,
35
- wait_ms: (delay * 1000).round,
36
- })
30
+ method: request.method.downcase.to_sym,
31
+ path: resource_path(uri),
32
+ attempt: attempt,
33
+ max_retries: @max_retries,
34
+ error: error,
35
+ wait_ms: (delay * 1000).round,
36
+ })
37
37
  end
38
38
 
39
39
  def handle_network_error(error, context, start)
@@ -133,8 +133,8 @@ module Clicksign
133
133
  modules, jsonapi = types.partition { |t| t.is_a?(Module) }
134
134
  if modules.any? && jsonapi.any?
135
135
  raise ArgumentError,
136
- 'cannot mix Module with JSON:API ' \
137
- 'include types — use with_includes for sideload'
136
+ 'cannot mix Module with JSON:API ' \
137
+ 'include types — use with_includes for sideload'
138
138
  end
139
139
  if modules.any?
140
140
  modules.each { |mod| super(mod) }
@@ -193,8 +193,8 @@ module Clicksign
193
193
  return if invalid.empty?
194
194
 
195
195
  raise ArgumentError,
196
- 'JSON:API include types must be String or Symbol, ' \
197
- "got: #{invalid.map(&:class).uniq.join(', ')}"
196
+ 'JSON:API include types must be String or Symbol, ' \
197
+ "got: #{invalid.map(&:class).uniq.join(', ')}"
198
198
  end
199
199
 
200
200
  private
@@ -212,8 +212,8 @@ module Clicksign
212
212
 
213
213
  loop do
214
214
  raw = client.get(endpoint,
215
- params: base.merge('page[number]' => page,
216
- 'page[size]' => per))
215
+ params: base.merge('page[number]' => page,
216
+ 'page[size]' => per))
217
217
  parsed = JsonApi::Parser.parse(raw)
218
218
  items = parsed[:data].map { |item| build_instance(item) }
219
219
  yield items
@@ -236,6 +236,8 @@ module Clicksign
236
236
  end
237
237
 
238
238
  def build_instance(data, parent_id: nil)
239
+ raise NotFoundError, 'API returned null data' if data.nil?
240
+
239
241
  instance = allocate
240
242
  instance.send(:load_data, data, parent_id: parent_id)
241
243
  instance
@@ -261,7 +263,10 @@ module Clicksign
261
263
  ),
262
264
  )
263
265
  parsed = JsonApi::Parser.parse(raw)
264
- load_data(parsed[:data].first, parent_id: @_parent_id)
266
+ data = parsed[:data].first
267
+ raise NotFoundError, 'API returned null data' if data.nil?
268
+
269
+ load_data(data, parent_id: @_parent_id)
265
270
  self
266
271
  end
267
272
 
@@ -273,7 +278,10 @@ module Clicksign
273
278
  def reload
274
279
  raw = self.class.client.get("#{base_path}/#{@id}")
275
280
  parsed = JsonApi::Parser.parse(raw)
276
- load_data(parsed[:data].first, parent_id: @_parent_id)
281
+ data = parsed[:data].first
282
+ raise NotFoundError, 'API returned null data' if data.nil?
283
+
284
+ load_data(data, parent_id: @_parent_id)
277
285
  self
278
286
  end
279
287
 
@@ -27,23 +27,23 @@ module Clicksign
27
27
 
28
28
  def self.list_events(envelope_id, **filters)
29
29
  nested_list(envelope_id, nested_type: 'events', as: Event,
30
- params: filter_params(**filters))
30
+ params: filter_params(**filters))
31
31
  end
32
32
 
33
33
  def self.list_documents(envelope_id, **filters)
34
34
  nested_list(envelope_id, nested_type: 'documents', as: Document,
35
- params: filter_params(**filters))
35
+ params: filter_params(**filters))
36
36
  end
37
37
 
38
38
  def self.list_signers(envelope_id, **filters)
39
39
  nested_list(envelope_id, nested_type: 'signers', as: Signer,
40
- params: filter_params(**filters))
40
+ params: filter_params(**filters))
41
41
  end
42
42
 
43
43
  def self.list_signature_watchers(envelope_id, **filters)
44
44
  nested_list(envelope_id, nested_type: 'signature_watchers',
45
- as: SignatureWatcher,
46
- params: filter_params(**filters))
45
+ as: SignatureWatcher,
46
+ params: filter_params(**filters))
47
47
  end
48
48
 
49
49
  def self.list_requirements(envelope_id, **filters)
@@ -6,7 +6,9 @@ module Clicksign
6
6
  class Event < Clicksign::Resource
7
7
  self.resource_type = 'events'
8
8
 
9
- def self.create_for_document(envelope_id:, document_id:, **attributes)
9
+ CUSTOM_KINDS = %w[token_email token_sms].freeze
10
+
11
+ def self.create(envelope_id:, document_id:, **attributes)
10
12
  raw = client.post(
11
13
  "/envelopes/#{envelope_id}/documents/#{document_id}/events",
12
14
  body: JsonApi::Serializer.dump(type: resource_type, attributes: attributes),
@@ -14,6 +16,50 @@ module Clicksign
14
16
  parsed = JsonApi::Parser.parse(raw)
15
17
  build_instance(parsed[:data].first)
16
18
  end
19
+
20
+ def self.create_add_image(envelope_id:, document_id:, title:, occurred_at:,
21
+ content_base64:)
22
+ create(
23
+ envelope_id: envelope_id,
24
+ document_id: document_id,
25
+ name: 'add_image',
26
+ content_base64: content_base64,
27
+ data: { title: title, occurred_at: occurred_at },
28
+ )
29
+ end
30
+
31
+ def self.create_custom(envelope_id:, document_id:, kind:, occurred_at:,
32
+ signer_name:, signer_email: nil, signer_phone_number: nil)
33
+ unless CUSTOM_KINDS.include?(kind.to_s)
34
+ raise ArgumentError, "kind must be one of: #{CUSTOM_KINDS.join(', ')}"
35
+ end
36
+
37
+ create(
38
+ envelope_id: envelope_id,
39
+ document_id: document_id,
40
+ name: 'custom',
41
+ data: {
42
+ kind: kind,
43
+ occurred_at: occurred_at,
44
+ signer_name: signer_name,
45
+ signer_email: signer_email,
46
+ signer_phone_number: signer_phone_number,
47
+ }.compact,
48
+ )
49
+ end
50
+
51
+ # API only exposes GET (list) and POST (create) for events — no singleton routes.
52
+ def update(**)
53
+ raise NotImplementedError, 'Event does not support update'
54
+ end
55
+
56
+ def delete
57
+ raise NotImplementedError, 'Event does not support delete'
58
+ end
59
+
60
+ def reload
61
+ raise NotImplementedError, 'Event does not support reload'
62
+ end
17
63
  end
18
64
  end
19
65
  end
@@ -27,7 +27,7 @@ module Clicksign
27
27
 
28
28
  def update(**)
29
29
  raise NotImplementedError,
30
- 'SignatureWatcher does not support update (route: except: [:update])'
30
+ 'SignatureWatcher does not support update (route: except: [:update])'
31
31
  end
32
32
 
33
33
  def envelope_id
@@ -40,7 +40,7 @@ module Clicksign
40
40
 
41
41
  def update(**)
42
42
  raise NotImplementedError,
43
- 'Signer does not support update (route: except: [:update])'
43
+ 'Signer does not support update (route: except: [:update])'
44
44
  end
45
45
 
46
46
  def base_path
@@ -3,7 +3,7 @@
3
3
  module Clicksign
4
4
  class Services
5
5
  def initialize(api_key:, environment: :production, base_url: nil,
6
- open_timeout: 2, read_timeout: 10, write_timeout: 10, max_retries: 0)
6
+ open_timeout: 2, read_timeout: 10, write_timeout: 10, max_retries: 0)
7
7
  resolved_url = base_url || resolve_environment(environment)
8
8
  @client = Client.new(
9
9
  api_key: api_key,
@@ -30,8 +30,10 @@ module Clicksign
30
30
 
31
31
  # Constant-time comparison to prevent timing attacks.
32
32
  def self.secure_compare?(expected, actual)
33
+ return false if actual.nil? || actual.to_s.empty?
34
+
33
35
  digest_a = OpenSSL::Digest::SHA256.hexdigest(expected)
34
- digest_b = OpenSSL::Digest::SHA256.hexdigest(actual)
36
+ digest_b = OpenSSL::Digest::SHA256.hexdigest(actual.to_s)
35
37
  result = 0
36
38
  digest_a.bytes.zip(digest_b.bytes) { |x, y| result |= x ^ y }
37
39
  result.zero?
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clicksign-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clicksign
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
77
  - !ruby/object:Gem::Version
78
78
  version: '0'
79
79
  requirements: []
80
- rubygems_version: 4.0.11
80
+ rubygems_version: 3.6.9
81
81
  specification_version: 4
82
82
  summary: Ruby SDK for the Clicksign API
83
83
  test_files: []