clicksign-ruby-sdk 0.1.5 → 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: a45728b201a9703e1d70cb6de98056f85da9a524d9d0c122a1ed951d6f98ef5b
4
- data.tar.gz: 7a2aea99029254b4849b27090f40927d4cbf66d44cb516f121ea14c65544f141
3
+ metadata.gz: a8730a44f32966cd0d6978150f4c5c5e821de5c168b94f4e69ced112cef211cc
4
+ data.tar.gz: afe6780086fe195e7559470d86674d83b8927c725e96213f1bb930a43808af54
5
5
  SHA512:
6
- metadata.gz: e9cb3bdaff221f8a8e58a9ef4ca47363962e19b7dcb4f22eb129cb1f5f3b60812550db76301ee8a36473be11490293932895d83c91ef60d10928206d090cd162
7
- data.tar.gz: 8c3cd41e15ba5c21136fc3ee4b53fcb96c231620735c17401acbd046a032262b31a9a1617f07ec704e9a9593dedb8023dd82019afe156cee782356c78c297199
6
+ metadata.gz: c939a985f4caf49b47d15b2b6637550e20083cf217f077bf0d50c042946b693db885bb8ccad98abc1bfb48a7465d5ebc8ed9c47eef4aac8f32a078c9560fcb12
7
+ data.tar.gz: 80ee966ee546d6b401c7e427afa7bd7b881b64fb342e18e068469bb24f8b043220b1edc3d853bb3b348e1fac95c4e68284bf14a8a8ef1ac4325b25d176ce7f84
data/README.md CHANGED
@@ -9,7 +9,7 @@ Cliente Ruby oficial para a [API v3 da Clicksign](https://developers.clicksign.c
9
9
 
10
10
  **Requisitos:** Ruby >= 3.0 · dependências de runtime: apenas biblioteca padrão (`net/http`, `json`).
11
11
 
12
- **Documentação:** [índice `docs/`](docs/) · [Workflow](docs/WORKFLOW.md) · [Cookbook](docs/cookbook/) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Arquitetura](docs/ARCHITECTURE.md) · [Observabilidade](docs/OBSERVABILITY.md) · [SPEC](docs/SPEC.md) · API: [Sandbox](https://sandbox.clicksign.com/api/v3) · [Produção](https://app.clicksign.com/api/v3)
12
+ **Documentação:** [índice `docs/`](docs/) · [Workflow](docs/WORKFLOW.md) · [Cookbook](docs/examples/) · [Troubleshooting](docs/TROUBLESHOOTING.md) · [Arquitetura](docs/ARCHITECTURE.md) · [Observabilidade](docs/OBSERVABILITY.md) · [SPEC](docs/SPEC.md) · API: [Sandbox](https://sandbox.clicksign.com/api/v3) · [Produção](https://app.clicksign.com/api/v3)
13
13
 
14
14
  ---
15
15
 
@@ -31,7 +31,7 @@ Cliente Ruby oficial para a [API v3 da Clicksign](https://developers.clicksign.c
31
31
 
32
32
  > **Exemplo passo a passo:** [`docs/WORKFLOW.md`](docs/WORKFLOW.md) — fluxo completo de envelope → documento → signatário → requisitos → ativação → notificação.
33
33
 
34
- > **Cookbook (receitas por cenário):** [`docs/cookbook/`](docs/cookbook/) — [retries](docs/cookbook/01-retries.md), [bulk requirements](docs/cookbook/02-bulk-requirements.md), [webhooks](docs/cookbook/03-webhooks.md), [vários clientes](docs/cookbook/04-multi-client.md), [list vs filter](docs/cookbook/07-list-and-filter.md), [limitações de produção](docs/cookbook/08-production-limitations.md).
34
+ > **Cookbook (receitas por cenário):** [`docs/examples/`](docs/examples/) — [retries](docs/examples/01-retries.md), [bulk requirements](docs/examples/02-bulk-requirements.md), [webhooks](docs/examples/03-webhooks.md), [vários clientes](docs/examples/04-multi-client.md), [list vs filter](docs/examples/07-list-and-filter.md), [limitações de produção](docs/examples/08-production-limitations.md).
35
35
 
36
36
  > **Troubleshooting:** [`docs/TROUBLESHOOTING.md`](docs/TROUBLESHOOTING.md) — sintoma → causa → correção (erros HTTP, multi-tenant, bulk parcial, webhooks).
37
37
 
@@ -507,7 +507,7 @@ Event.create(
507
507
 
508
508
  `list` **não** aceita argumentos. Para filtrar: `Envelope.filter(status: 'draft').to_a` (não `Envelope.list(status: 'draft')`).
509
509
 
510
- Guia completo: [`docs/cookbook/07-list-and-filter.md`](docs/cookbook/07-list-and-filter.md).
510
+ Guia completo: [`docs/examples/07-list-and-filter.md`](docs/examples/07-list-and-filter.md).
511
511
 
512
512
  ```ruby
513
513
  # Sem filtros — retorna Array imediatamente
@@ -720,7 +720,7 @@ Cada request abre e fecha uma conexão TCP (via `Net::HTTP.start`). Não há reu
720
720
  - **OK** para jobs sequenciais, integrações moderadas e a maioria dos apps Rails.
721
721
  - **Atenção** em Puma com muitas threads e várias chamadas Clicksign por request: overhead de handshake/TLS pode virar gargalo antes do rate limit da API.
722
722
 
723
- Mitigações: menos round-trips (`BulkRequirement`, batch na app), filas (Sidekiq), cache de leitura. Detalhes: [`docs/cookbook/08-production-limitations.md`](docs/cookbook/08-production-limitations.md).
723
+ Mitigações: menos round-trips (`BulkRequirement`, batch na app), filas (Sidekiq), cache de leitura. Detalhes: [`docs/examples/08-production-limitations.md`](docs/examples/08-production-limitations.md).
724
724
 
725
725
  ### `Thread.current` e Fibers
726
726
 
@@ -765,7 +765,7 @@ lib/clicksign/
765
765
  docs/SPEC.md # mapa completo de resources e rotas
766
766
  docs/WORKFLOW.md # fluxo notarial ponta a ponta
767
767
  docs/README.md # índice da documentação
768
- docs/cookbook/ # receitas: retries, bulk, webhooks, multi-cliente
768
+ docs/examples/ # receitas: retries, bulk, webhooks, multi-cliente
769
769
  docs/TROUBLESHOOTING.md # diagnóstico e erros comuns
770
770
  docs/ARCHITECTURE.md # diagramas e camadas
771
771
  docs/OBSERVABILITY.md # logs, métricas, OpenTelemetry
data/REVISION ADDED
@@ -0,0 +1 @@
1
+ 0.1.8
@@ -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: 3)
18
18
  @api_key = api_key
19
19
  @base_url = base_url
20
20
  @open_timeout = open_timeout
@@ -42,6 +42,13 @@ module Clicksign
42
42
  execute_with_retry(request, uri)
43
43
  end
44
44
 
45
+ def put(path, body:)
46
+ uri = build_uri(path)
47
+ request = Net::HTTP::Put.new(uri, headers)
48
+ request.body = body.to_json
49
+ execute_with_retry(request, uri)
50
+ end
51
+
45
52
  def delete(path, body: nil)
46
53
  uri = build_uri(path)
47
54
  request = Net::HTTP::Delete.new(uri, headers)
@@ -70,7 +77,7 @@ module Clicksign
70
77
  Clicksign::ServerError => e
71
78
  raise unless e.retryable? && attempts <= @max_retries
72
79
 
73
- delay = RetryBackoff.delay(attempts)
80
+ delay = RetryBackoff.retry_delay(attempts, e.response_headers)
74
81
  publish_retry(request, uri, attempts, e, delay)
75
82
  sleep(delay)
76
83
  retry
@@ -82,8 +89,9 @@ module Clicksign
82
89
  context = request_context(request, uri, attempt)
83
90
  response = http_request(request, uri)
84
91
  handle_response(response, context, start)
85
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
86
- handle_network_error(e, context, elapsed_ms(start))
92
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout,
93
+ Errno::ECONNREFUSED => e
94
+ handle_network_error(e, context, start)
87
95
  end
88
96
 
89
97
  def http_request(request, uri)
@@ -15,7 +15,7 @@ module Clicksign
15
15
  @open_timeout = 2
16
16
  @read_timeout = 10
17
17
  @write_timeout = 10
18
- @max_retries = 0
18
+ @max_retries = 3
19
19
  end
20
20
 
21
21
  def environment=(env)
@@ -50,7 +50,9 @@ module Clicksign
50
50
  errors = body['errors']
51
51
  return response.message unless errors.is_a?(Array)
52
52
 
53
- result = errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
53
+ result = errors.filter_map do |e|
54
+ e.is_a?(Hash) && (e['detail'] || e['title'])
55
+ end.join(', ')
54
56
  result.empty? ? response.message : result
55
57
  end
56
58
  end
@@ -24,6 +24,7 @@ module Clicksign
24
24
  end
25
25
 
26
26
  def build_operation_result(slot:, index:, op:, envelope_id:)
27
+ slot ||= {}
27
28
  errors = slot['errors']
28
29
  requirement = build_requirement(slot['data'], envelope_id: envelope_id)
29
30
 
@@ -37,7 +38,7 @@ module Clicksign
37
38
  end
38
39
 
39
40
  def build_requirement(data, envelope_id:)
40
- return nil if data.nil? || data.empty?
41
+ return nil if data.nil? || (!data['id'] && !data['type'])
41
42
 
42
43
  Resources::Notarial::Requirement.send(
43
44
  :build_instance,
@@ -54,7 +55,7 @@ module Clicksign
54
55
  def format_errors(errors)
55
56
  return 'Validation failed' unless errors.is_a?(Array)
56
57
 
57
- errors.filter_map { |e| e['detail'] || e['title'] }.join(', ')
58
+ errors.filter_map { |e| e.is_a?(Hash) && (e['detail'] || e['title']) }.join(', ')
58
59
  end
59
60
  end
60
61
  end
@@ -43,7 +43,8 @@ module Clicksign
43
43
  begin
44
44
  attempts += 1
45
45
  execute_once(request, uri, attempt: attempts)
46
- rescue Clicksign::TimeoutError => e
46
+ rescue Clicksign::TimeoutError, Clicksign::RateLimitError,
47
+ Clicksign::ServerError => e
47
48
  raise unless e.retryable? && attempts <= @max_retries
48
49
 
49
50
  delay = RetryBackoff.delay(attempts)
@@ -64,8 +65,8 @@ module Clicksign
64
65
  end
65
66
 
66
67
  def handle_bulk_body(response, context, status, duration)
67
- parsed = parse_response_body(response) || {}
68
- return parsed if parsed.key?('atomic:results')
68
+ parsed = parse_response_body(response)
69
+ return parsed if parsed&.key?('atomic:results')
69
70
 
70
71
  begin
71
72
  ErrorHandler.call(response)
@@ -73,7 +74,7 @@ module Clicksign
73
74
  publish_http_error(context, e, status, duration)
74
75
  raise
75
76
  end
76
- parsed
77
+ parsed || {}
77
78
  end
78
79
 
79
80
  def http_post(request, uri)
@@ -4,11 +4,14 @@ module Clicksign
4
4
  module JsonApi
5
5
  module Parser
6
6
  def self.parse(raw)
7
+ return { data: [], included: [], links: nil } if raw.nil?
8
+
7
9
  raw_data = raw['data']
8
10
  data = case raw_data
9
11
  when Array then raw_data.map { |item| build(item) }
10
12
  when Hash then [build(raw_data)]
11
- else []
13
+ when nil then []
14
+ else raise Error, "Unexpected JSON:API data type: #{raw_data.class}"
12
15
  end
13
16
 
14
17
  included = Array(raw['included'])
@@ -3,8 +3,8 @@
3
3
  module Clicksign
4
4
  module JsonApi
5
5
  module Serializer
6
- def self.dump(type:, attributes:, id: nil, relationships: {})
7
- data = { type: type, attributes: attributes }
6
+ def self.dump(type:, attributes: {}, id: nil, relationships: {})
7
+ data = { type: type, attributes: attributes || {} }
8
8
  data[:id] = id if id
9
9
  data[:relationships] = relationships unless relationships.empty?
10
10
  { data: data }
@@ -232,7 +232,7 @@ module Clicksign
232
232
  return true
233
233
  end
234
234
 
235
- items.size >= per
235
+ per.positive? && items.size >= per
236
236
  end
237
237
 
238
238
  def build_instance(data, parent_id: nil)
@@ -15,7 +15,9 @@ module Clicksign
15
15
  end
16
16
 
17
17
  def child_folder_ids
18
- Array(relationships.dig('folders', 'data')).filter_map { |d| d['id'] }
18
+ Array(relationships.dig('folders', 'data')).filter_map do |d|
19
+ d.is_a?(Hash) ? d['id'] : nil
20
+ end
19
21
  end
20
22
  end
21
23
  end
@@ -13,6 +13,21 @@ module Clicksign
13
13
  )
14
14
  end
15
15
 
16
+ def update(**attributes)
17
+ raw = self.class.client.put(
18
+ "#{base_path}/#{@id}",
19
+ body: JsonApi::Serializer.dump(
20
+ type: self.class.resource_type, id: @id, attributes: attributes,
21
+ ),
22
+ )
23
+ parsed = JsonApi::Parser.parse(raw)
24
+ data = parsed[:data].first
25
+ raise NotFoundError, 'API returned null data' if data.nil?
26
+
27
+ load_data(data, parent_id: @_parent_id)
28
+ self
29
+ end
30
+
16
31
  def user_id
17
32
  relationships.dig('user', 'data', 'id')
18
33
  end
@@ -42,6 +42,10 @@ module Clicksign
42
42
  end
43
43
 
44
44
  def self.create(envelope_id:, &block)
45
+ if envelope_id.nil? || envelope_id.to_s.empty?
46
+ raise ArgumentError,
47
+ 'envelope_id is required'
48
+ end
45
49
  raise ArgumentError, 'block is required' unless block
46
50
 
47
51
  ops = JsonApi::Operations::BulkRequirement.new
@@ -28,7 +28,13 @@ module Clicksign
28
28
  end
29
29
 
30
30
  def base_path
31
- "/envelopes/#{@_parent_id || envelope_id}/documents"
31
+ eid = @_parent_id || envelope_id
32
+ if eid.nil?
33
+ raise Clicksign::Error,
34
+ 'envelope_id is required for Document operations'
35
+ end
36
+
37
+ "/envelopes/#{eid}/documents"
32
38
  end
33
39
 
34
40
  def envelope_id
@@ -6,6 +6,8 @@ module Clicksign
6
6
  class Event < Clicksign::Resource
7
7
  self.resource_type = 'events'
8
8
 
9
+ # Known custom event kinds. Override or append to extend without SDK update:
10
+ # Clicksign::Resources::Notarial::Event::CUSTOM_KINDS << 'new_kind'
9
11
  CUSTOM_KINDS = %w[token_email token_sms].freeze
10
12
 
11
13
  def self.create(envelope_id:, document_id:, **attributes)
@@ -42,8 +42,19 @@ module Clicksign
42
42
  end
43
43
  private_class_method :list_related
44
44
 
45
+ def update(**)
46
+ raise NotImplementedError,
47
+ 'Requirement does not support update (API does not provide this endpoint)'
48
+ end
49
+
45
50
  def base_path
46
- "/envelopes/#{@_parent_id || envelope_id}/requirements"
51
+ eid = @_parent_id || envelope_id
52
+ if eid.nil?
53
+ raise Clicksign::Error,
54
+ 'envelope_id is required for Requirement operations'
55
+ end
56
+
57
+ "/envelopes/#{eid}/requirements"
47
58
  end
48
59
 
49
60
  def envelope_id
@@ -22,7 +22,13 @@ module Clicksign
22
22
  end
23
23
 
24
24
  def base_path
25
- "/envelopes/#{@_parent_id || envelope_id}/signature_watchers"
25
+ eid = @_parent_id || envelope_id
26
+ if eid.nil?
27
+ raise Clicksign::Error,
28
+ 'envelope_id is required for SignatureWatcher operations'
29
+ end
30
+
31
+ "/envelopes/#{eid}/signature_watchers"
26
32
  end
27
33
 
28
34
  def update(**)
@@ -44,7 +44,13 @@ module Clicksign
44
44
  end
45
45
 
46
46
  def base_path
47
- "/envelopes/#{@_parent_id || envelope_id}/signers"
47
+ eid = @_parent_id || envelope_id
48
+ if eid.nil?
49
+ raise Clicksign::Error,
50
+ 'envelope_id is required for Signer operations'
51
+ end
52
+
53
+ "/envelopes/#{eid}/signers"
48
54
  end
49
55
 
50
56
  def envelope_id
@@ -18,5 +18,22 @@ module Clicksign
18
18
 
19
19
  rng.rand(max)
20
20
  end
21
+
22
+ def parse_retry_after(headers)
23
+ return nil unless headers.is_a?(Hash)
24
+
25
+ raw = headers['retry-after'] || headers['Retry-After']
26
+ return nil if raw.nil? || raw.to_s.strip.empty?
27
+
28
+ Float(raw.to_s.strip)
29
+ rescue ArgumentError, TypeError
30
+ nil
31
+ end
32
+
33
+ def retry_delay(attempt, headers = nil, rng: Random)
34
+ jitter = delay(attempt, rng: rng)
35
+ retry_after = parse_retry_after(headers)
36
+ retry_after ? [jitter, retry_after].max : jitter
37
+ end
21
38
  end
22
39
  end
@@ -25,18 +25,21 @@ module Clicksign
25
25
 
26
26
  # Computes the expected Content-HMAC value for a given payload and secret.
27
27
  def self.compute_signature(payload, secret:)
28
+ raise ArgumentError, 'secret must not be empty' if secret.nil? || secret.empty?
29
+
28
30
  "#{DIGEST}=#{OpenSSL::HMAC.hexdigest(DIGEST, secret, payload)}"
29
31
  end
30
32
 
31
33
  # Constant-time comparison to prevent timing attacks.
32
34
  def self.secure_compare?(expected, actual)
33
- return false if actual.nil? || actual.to_s.empty?
35
+ return false if actual.nil?
36
+
37
+ actual_str = actual.is_a?(String) ? actual : actual.to_s
38
+ return false if actual_str.empty?
34
39
 
35
- digest_a = OpenSSL::Digest::SHA256.hexdigest(expected)
36
- digest_b = OpenSSL::Digest::SHA256.hexdigest(actual.to_s)
37
- result = 0
38
- digest_a.bytes.zip(digest_b.bytes) { |x, y| result |= x ^ y }
39
- result.zero?
40
+ OpenSSL.fixed_length_secure_compare(expected, actual_str)
41
+ rescue ArgumentError
42
+ false
40
43
  end
41
44
  private_class_method :secure_compare?
42
45
  end
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.5
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clicksign
@@ -17,6 +17,7 @@ extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
19
  - README.md
20
+ - REVISION
20
21
  - lib/clicksign.rb
21
22
  - lib/clicksign/client.rb
22
23
  - lib/clicksign/configuration.rb