clicksign-ruby-sdk 0.1.4 → 0.1.7

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: 5d76bec5fed9c775d1277ef1d0cd7158a87f7234d71fcd2fc8933a4421c8dba1
4
+ data.tar.gz: d18db7bdcf3825e7d266ff30609f9034c0a8e2fbb3d7f82352ae1e4a0c7ef0dd
5
5
  SHA512:
6
- metadata.gz: 86b4b41f6607d89ea58786225502c699902523d05ff72f60d983449fc9a318d794ab1c164ce1d475a073ea868d22ee65da53f7e36c61cb477e28e4cea7fa86f4
7
- data.tar.gz: 484d16275cda5bf7e85413dcc3383776988574de052d6715a7d08e591d54701b13e3dd52a69c9a000977f8d51cc07a7590968050c6456b35cec48c914086a737
6
+ metadata.gz: d1f36573b8720bb2d5548540cabcb1b9d995b8cc6afb877e6b02844cef69a73af2708901c2f50dd43ca540db7f098c61077bfc9647a5e880928c361707da9069
7
+ data.tar.gz: d7b63661a2702eaaecfd59d65508964110965ed32a4dd26f273199951c7353f9761a8f5269389e8aec83044c3bc54562e458fa885180817f17b0ad19fec59b67
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
 
@@ -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
 
@@ -484,7 +507,7 @@ Event.create_for_document(
484
507
 
485
508
  `list` **não** aceita argumentos. Para filtrar: `Envelope.filter(status: 'draft').to_a` (não `Envelope.list(status: 'draft')`).
486
509
 
487
- 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).
488
511
 
489
512
  ```ruby
490
513
  # Sem filtros — retorna Array imediatamente
@@ -697,7 +720,7 @@ Cada request abre e fecha uma conexão TCP (via `Net::HTTP.start`). Não há reu
697
720
  - **OK** para jobs sequenciais, integrações moderadas e a maioria dos apps Rails.
698
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.
699
722
 
700
- 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).
701
724
 
702
725
  ### `Thread.current` e Fibers
703
726
 
@@ -742,7 +765,7 @@ lib/clicksign/
742
765
  docs/SPEC.md # mapa completo de resources e rotas
743
766
  docs/WORKFLOW.md # fluxo notarial ponta a ponta
744
767
  docs/README.md # índice da documentação
745
- docs/cookbook/ # receitas: retries, bulk, webhooks, multi-cliente
768
+ docs/examples/ # receitas: retries, bulk, webhooks, multi-cliente
746
769
  docs/TROUBLESHOOTING.md # diagnóstico e erros comuns
747
770
  docs/ARCHITECTURE.md # diagramas e camadas
748
771
  docs/OBSERVABILITY.md # logs, métricas, OpenTelemetry
data/REVISION ADDED
@@ -0,0 +1 @@
1
+ 0.1.7
@@ -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
@@ -82,17 +82,18 @@ module Clicksign
82
82
  context = request_context(request, uri, attempt)
83
83
  response = http_request(request, uri)
84
84
  handle_response(response, context, start)
85
- rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED => e
86
- handle_network_error(e, context, elapsed_ms(start))
85
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout,
86
+ Errno::ECONNREFUSED => e
87
+ handle_network_error(e, context, start)
87
88
  end
88
89
 
89
90
  def http_request(request, uri)
90
91
  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) })
92
+ use_ssl: uri.scheme == 'https',
93
+ open_timeout: @open_timeout,
94
+ read_timeout: @read_timeout,
95
+ write_timeout: @write_timeout,
96
+ &proc { |http| http.request(request) })
96
97
  end
97
98
 
98
99
  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
@@ -37,7 +37,7 @@ module Clicksign
37
37
  end
38
38
 
39
39
  def build_requirement(data, envelope_id:)
40
- return nil if data.nil? || data.empty?
40
+ return nil if data.nil? || (!data['id'] && !data['type'])
41
41
 
42
42
  Resources::Notarial::Requirement.send(
43
43
  :build_instance,
@@ -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
@@ -64,8 +64,8 @@ module Clicksign
64
64
  end
65
65
 
66
66
  def handle_bulk_body(response, context, status, duration)
67
- parsed = parse_response_body(response) || {}
68
- return parsed if parsed.key?('atomic:results')
67
+ parsed = parse_response_body(response)
68
+ return parsed if parsed&.key?('atomic:results')
69
69
 
70
70
  begin
71
71
  ErrorHandler.call(response)
@@ -73,16 +73,16 @@ module Clicksign
73
73
  publish_http_error(context, e, status, duration)
74
74
  raise
75
75
  end
76
- parsed
76
+ parsed || {}
77
77
  end
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'
@@ -4,6 +4,8 @@ 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) }
@@ -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
 
@@ -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 }
@@ -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
 
@@ -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
@@ -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,11 @@ 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
+ # Known custom event kinds. Override or append to extend without SDK update:
10
+ # Clicksign::Resources::Notarial::Event::CUSTOM_KINDS << 'new_kind'
11
+ CUSTOM_KINDS = %w[token_email token_sms].freeze
12
+
13
+ def self.create(envelope_id:, document_id:, **attributes)
10
14
  raw = client.post(
11
15
  "/envelopes/#{envelope_id}/documents/#{document_id}/events",
12
16
  body: JsonApi::Serializer.dump(type: resource_type, attributes: attributes),
@@ -14,6 +18,50 @@ module Clicksign
14
18
  parsed = JsonApi::Parser.parse(raw)
15
19
  build_instance(parsed[:data].first)
16
20
  end
21
+
22
+ def self.create_add_image(envelope_id:, document_id:, title:, occurred_at:,
23
+ content_base64:)
24
+ create(
25
+ envelope_id: envelope_id,
26
+ document_id: document_id,
27
+ name: 'add_image',
28
+ content_base64: content_base64,
29
+ data: { title: title, occurred_at: occurred_at },
30
+ )
31
+ end
32
+
33
+ def self.create_custom(envelope_id:, document_id:, kind:, occurred_at:,
34
+ signer_name:, signer_email: nil, signer_phone_number: nil)
35
+ unless CUSTOM_KINDS.include?(kind.to_s)
36
+ raise ArgumentError, "kind must be one of: #{CUSTOM_KINDS.join(', ')}"
37
+ end
38
+
39
+ create(
40
+ envelope_id: envelope_id,
41
+ document_id: document_id,
42
+ name: 'custom',
43
+ data: {
44
+ kind: kind,
45
+ occurred_at: occurred_at,
46
+ signer_name: signer_name,
47
+ signer_email: signer_email,
48
+ signer_phone_number: signer_phone_number,
49
+ }.compact,
50
+ )
51
+ end
52
+
53
+ # API only exposes GET (list) and POST (create) for events — no singleton routes.
54
+ def update(**)
55
+ raise NotImplementedError, 'Event does not support update'
56
+ end
57
+
58
+ def delete
59
+ raise NotImplementedError, 'Event does not support delete'
60
+ end
61
+
62
+ def reload
63
+ raise NotImplementedError, 'Event does not support reload'
64
+ end
17
65
  end
18
66
  end
19
67
  end
@@ -43,7 +43,13 @@ module Clicksign
43
43
  private_class_method :list_related
44
44
 
45
45
  def base_path
46
- "/envelopes/#{@_parent_id || envelope_id}/requirements"
46
+ eid = @_parent_id || envelope_id
47
+ if eid.nil?
48
+ raise Clicksign::Error,
49
+ 'envelope_id is required for Requirement operations'
50
+ end
51
+
52
+ "/envelopes/#{eid}/requirements"
47
53
  end
48
54
 
49
55
  def envelope_id
@@ -22,12 +22,18 @@ 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(**)
29
35
  raise NotImplementedError,
30
- 'SignatureWatcher does not support update (route: except: [:update])'
36
+ 'SignatureWatcher does not support update (route: except: [:update])'
31
37
  end
32
38
 
33
39
  def envelope_id
@@ -40,11 +40,17 @@ 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
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
@@ -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,
@@ -25,16 +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
- digest_a = OpenSSL::Digest::SHA256.hexdigest(expected)
34
- digest_b = OpenSSL::Digest::SHA256.hexdigest(actual)
35
- result = 0
36
- digest_a.bytes.zip(digest_b.bytes) { |x, y| result |= x ^ y }
37
- result.zero?
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?
39
+
40
+ OpenSSL.fixed_length_secure_compare(expected, actual_str)
41
+ rescue ArgumentError
42
+ false
38
43
  end
39
44
  private_class_method :secure_compare?
40
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.4
4
+ version: 0.1.7
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
@@ -77,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
78
  - !ruby/object:Gem::Version
78
79
  version: '0'
79
80
  requirements: []
80
- rubygems_version: 4.0.11
81
+ rubygems_version: 3.6.9
81
82
  specification_version: 4
82
83
  summary: Ruby SDK for the Clicksign API
83
84
  test_files: []