autentique 0.1.0

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.
data/README.md ADDED
@@ -0,0 +1,504 @@
1
+ # Autentique Ruby Gem
2
+
3
+ A Ruby client for the [Autentique](https://autentique.com.br/) digital signature API. This gem provides a clean, idiomatic Ruby interface to Autentique's GraphQL API for document signing and management.
4
+
5
+ [![CI](https://github.com/keithyoder/autentique-ruby/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/keithyoder/autentique-ruby/actions/workflows/ci.yml)
6
+ [![Security](https://github.com/keithyoder/autentique-ruby/actions/workflows/security.yml/badge.svg?branch=main)](https://github.com/keithyoder/autentique-ruby/actions/workflows/security.yml)
7
+ [![Gem Version](https://badge.fury.io/rb/nfcom.svg)](https://badge.fury.io/rb/autentique)
8
+
9
+ ## Features
10
+
11
+ - 🔐 **Simple Authentication** - Easy API key configuration
12
+ - 📄 **Document Management** - Create, retrieve, list, and delete documents
13
+ - ✍️ **Flexible Signing** - Support for multiple signers with various authentication methods
14
+ - 🏖️ **Sandbox Mode** - Test without consuming document credits
15
+ - 🔍 **Advanced Queries** - Full GraphQL query support
16
+ - 📁 **Folder Management** - Organize documents in folders
17
+ - 🛡️ **Type Safety** - Model classes for structured data
18
+ - ⚡ **Rate Limiting** - Built-in rate limit handling (60 requests/minute)
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ ```ruby
25
+ gem 'autentique'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ Or install it yourself as:
35
+
36
+ ```bash
37
+ gem install autentique
38
+ ```
39
+
40
+ ## Configuration
41
+
42
+ ### Using Environment Variables
43
+
44
+ ```ruby
45
+ # Set your API key as an environment variable
46
+ ENV['AUTENTIQUE_API_KEY'] = 'your_api_key_here'
47
+
48
+ # Create a client
49
+ client = Autentique::Client.new(api_key: ENV['AUTENTIQUE_API_KEY'])
50
+ ```
51
+
52
+ ### Global Configuration
53
+
54
+ ```ruby
55
+ Autentique.configure do |config|
56
+ config.api_key = 'your_api_key_here'
57
+ config.sandbox = false # Set to true for testing
58
+ end
59
+
60
+ # Use the configured client
61
+ client = Autentique.client
62
+ ```
63
+
64
+ ### Direct Initialization
65
+
66
+ ```ruby
67
+ client = Autentique::Client.new(
68
+ api_key: 'your_api_key_here',
69
+ sandbox: false
70
+ )
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ ### Creating Documents
76
+
77
+ #### Basic Document Creation
78
+
79
+ ```ruby
80
+ client = Autentique::Client.new(api_key: 'your_api_key')
81
+
82
+ document = client.documents.create(
83
+ file: '/path/to/contract.pdf',
84
+ document: {
85
+ name: 'Employment Contract'
86
+ },
87
+ signers: [
88
+ {
89
+ email: 'employee@example.com',
90
+ action: 'SIGN'
91
+ }
92
+ ]
93
+ )
94
+
95
+ puts "Document created: #{document.id}"
96
+ puts "Signature link: #{document.signatures.first.short_link}"
97
+ ```
98
+
99
+ #### Advanced Document Creation
100
+
101
+ ```ruby
102
+ document = client.documents.create(
103
+ file: File.open('/path/to/contract.pdf'),
104
+ document: {
105
+ name: 'Marketing Contract',
106
+ message: 'Please review and sign this contract',
107
+ reminder: 'WEEKLY', # Send weekly reminders
108
+ sortable: true, # Signers must sign in order
109
+ refusable: true, # Allow document rejection
110
+ qualified: true, # Enable qualified signatures
111
+ scrolling_required: true, # Require full scroll before signing
112
+ stop_on_rejected: true, # Stop process if rejected
113
+ new_signature_style: true, # Use new signature fields
114
+ deadline_at: '2025-12-31T23:59:59.999Z',
115
+ configs: {
116
+ notification_finished: true, # Notify when all signed
117
+ notification_signed: true, # Notify signer after signing
118
+ signature_appearance: 'DRAW' # Force signature style
119
+ }
120
+ },
121
+ signers: [
122
+ {
123
+ email: 'signer1@example.com',
124
+ action: 'SIGN',
125
+ configs: { cpf: '12345678900' }, # Validate CPF
126
+ positions: [
127
+ { x: 5.0, y: 90.0, z: 1, element: 'SIGNATURE' }
128
+ ]
129
+ },
130
+ {
131
+ name: 'Witness Name',
132
+ action: 'SIGN_AS_A_WITNESS',
133
+ positions: [
134
+ { x: 75.0, y: 90.0, z: 1, element: 'NAME' }
135
+ ]
136
+ },
137
+ {
138
+ phone: '+5554999999999',
139
+ delivery_method: 'DELIVERY_METHOD_WHATSAPP',
140
+ action: 'SIGN',
141
+ security_verifications: [
142
+ { type: 'SMS', verify_phone: '+5554999999999' }
143
+ ]
144
+ }
145
+ ],
146
+ folder_id: 'folder-uuid' # Optional: organize in folder
147
+ )
148
+ ```
149
+
150
+ #### Document Actions
151
+
152
+ Signers can perform different actions:
153
+ - `SIGN` - Sign the document
154
+ - `SIGN_AS_A_WITNESS` - Sign as a witness
155
+ - `APPROVE` - Approve the document
156
+ - `RECOGNIZE` - Acknowledge the document
157
+
158
+ #### Delivery Methods
159
+
160
+ For phone-based signers:
161
+ - `DELIVERY_METHOD_WHATSAPP` - Send via WhatsApp
162
+ - `DELIVERY_METHOD_SMS` - Send via SMS
163
+
164
+ #### Security Verifications
165
+
166
+ Add extra security layers:
167
+
168
+ ```ruby
169
+ signers: [
170
+ {
171
+ email: 'signer@example.com',
172
+ action: 'SIGN',
173
+ security_verifications: [
174
+ { type: 'SMS', verify_phone: '+5554999999999' }, # SMS verification
175
+ { type: 'MANUAL' }, # Manual photo ID approval
176
+ { type: 'UPLOAD' }, # Photo ID upload
177
+ { type: 'LIVE' }, # Selfie + liveness check
178
+ { type: 'PF_FACIAL' }, # SERPRO biometric
179
+ { type: 'BIOMETRIC_AND_TEXT_EXTRACTION' } # Photo ID + facematch
180
+ ]
181
+ }
182
+ ]
183
+ ```
184
+
185
+ ### Retrieving Documents
186
+
187
+ ```ruby
188
+ # Get a specific document
189
+ document = client.documents.find('document-uuid')
190
+
191
+ puts "Document: #{document.name}"
192
+ puts "Status: #{document.signed? ? 'Signed' : 'Pending'}"
193
+
194
+ document.signatures.each do |signature|
195
+ puts "Signer: #{signature.email}"
196
+ puts "Status: #{signature.signed? ? 'Signed' : 'Pending'}"
197
+ puts "Link: #{signature.short_link}" if signature.pending?
198
+ end
199
+ ```
200
+
201
+ ### Listing Documents
202
+
203
+ ```ruby
204
+ # List pending documents
205
+ pending = client.documents.pending(limit: 20, page: 1)
206
+
207
+ pending.each do |doc|
208
+ puts "#{doc.name} - #{doc.id}"
209
+ end
210
+
211
+ # List all documents with filter
212
+ signed_docs = client.documents.list(status: 'SIGNED', limit: 50)
213
+ rejected_docs = client.documents.list(status: 'REJECTED')
214
+ all_docs = client.documents.list(limit: 100)
215
+ ```
216
+
217
+ ### Deleting Documents
218
+
219
+ ```ruby
220
+ client.documents.delete('document-uuid')
221
+ ```
222
+
223
+ ### Working with Folders
224
+
225
+ ```ruby
226
+ # List folders
227
+ folders = client.folders.list
228
+ folders.each { |f| puts "#{f['name']} - #{f['id']}" }
229
+
230
+ # Create a folder
231
+ folder = client.folders.create(name: 'Contracts 2025')
232
+ puts "Created folder: #{folder['id']}"
233
+
234
+ # Delete a folder
235
+ client.folders.delete(id: 'folder-uuid')
236
+ ```
237
+
238
+ ### Sandbox Mode
239
+
240
+ Test without consuming document credits:
241
+
242
+ ```ruby
243
+ # Enable sandbox globally
244
+ client = Autentique::Client.new(
245
+ api_key: 'your_api_key',
246
+ sandbox: true
247
+ )
248
+
249
+ # Or per request
250
+ document = client.documents.create(
251
+ file: '/path/to/test.pdf',
252
+ document: { name: 'Test Doc' },
253
+ signers: [{ email: 'test@example.com', action: 'SIGN' }],
254
+ sandbox: true
255
+ )
256
+ ```
257
+
258
+ ### Using Model Classes
259
+
260
+ For better type safety and IDE support:
261
+
262
+ ```ruby
263
+ # Use DocumentInput model
264
+ doc_input = Autentique::Models::DocumentInput.new(
265
+ name: 'Contract',
266
+ reminder: 'WEEKLY',
267
+ refusable: true
268
+ )
269
+
270
+ # Use SignerInput model
271
+ signer = Autentique::Models::SignerInput.new(
272
+ email: 'signer@example.com',
273
+ action: 'SIGN',
274
+ positions: [
275
+ { x: 10.0, y: 90.0, z: 1, element: 'SIGNATURE' }
276
+ ]
277
+ )
278
+
279
+ document = client.documents.create(
280
+ file: 'contract.pdf',
281
+ document: doc_input,
282
+ signers: [signer]
283
+ )
284
+ ```
285
+
286
+ ### Rails Integration
287
+
288
+ #### Initializer
289
+
290
+ Create `config/initializers/autentique.rb`:
291
+
292
+ ```ruby
293
+ Autentique.configure do |config|
294
+ config.api_key = Rails.application.credentials.dig(:autentique, :api_key)
295
+ config.sandbox = Rails.env.development? || Rails.env.test?
296
+ end
297
+ ```
298
+
299
+ #### In Your Models
300
+
301
+ ```ruby
302
+ class Contrato < ApplicationRecord
303
+ belongs_to :pessoa
304
+
305
+ def enviar_para_assinatura
306
+ client = Autentique.client
307
+
308
+ documento = client.documents.create(
309
+ file: gerar_pdf,
310
+ document: {
311
+ name: "Contrato #{id}",
312
+ message: 'Por favor, assine este contrato'
313
+ },
314
+ signers: [
315
+ {
316
+ email: pessoa.email,
317
+ action: 'SIGN',
318
+ configs: { cpf: pessoa.cpf }
319
+ }
320
+ ]
321
+ )
322
+
323
+ update(
324
+ documentos: (documentos || []) << {
325
+ 'id' => documento.id,
326
+ 'nome' => documento.name,
327
+ 'data' => documento.created_at
328
+ }
329
+ )
330
+
331
+ documento
332
+ end
333
+
334
+ def verificar_assinatura(documento_id)
335
+ client = Autentique.client
336
+ documento = client.documents.find(documento_id)
337
+
338
+ if documento.signed?
339
+ update(status: :assinado)
340
+ elsif documento.rejected?
341
+ update(status: :rejeitado)
342
+ end
343
+
344
+ documento
345
+ end
346
+ end
347
+ ```
348
+
349
+ ### Error Handling
350
+
351
+ ```ruby
352
+ begin
353
+ document = client.documents.create(
354
+ file: 'contract.pdf',
355
+ document: { name: 'Contract' },
356
+ signers: [{ email: 'invalid@email', action: 'SIGN' }]
357
+ )
358
+ rescue Autentique::AuthenticationError => e
359
+ puts "Authentication failed: #{e.message}"
360
+ rescue Autentique::RateLimitError => e
361
+ puts "Rate limit exceeded: #{e.message}"
362
+ rescue Autentique::ValidationError => e
363
+ puts "Validation error: #{e.message}"
364
+ rescue Autentique::QueryError => e
365
+ puts "Query failed: #{e.message}"
366
+ puts "Errors: #{e.errors.inspect}"
367
+ rescue Autentique::UploadError => e
368
+ puts "Upload failed: #{e.message}"
369
+ rescue Autentique::Error => e
370
+ puts "General error: #{e.message}"
371
+ end
372
+ ```
373
+
374
+ ## API Coverage
375
+
376
+ ### Implemented
377
+
378
+ - ✅ Create documents with file upload
379
+ - ✅ Retrieve document by ID
380
+ - ✅ List pending documents
381
+ - ✅ List all documents with filters
382
+ - ✅ Delete documents
383
+ - ✅ List folders
384
+ - ✅ Create folders
385
+ - ✅ Delete folders
386
+ - ✅ Sandbox mode support
387
+ - ✅ All signer options
388
+ - ✅ Security verifications
389
+ - ✅ Signature positioning
390
+ - ✅ Document configurations
391
+
392
+ ### Roadmap
393
+
394
+ - ⏳ Webhooks support
395
+ - ⏳ Document templates
396
+ - ⏳ Bulk operations
397
+ - ⏳ Organization management
398
+ - ⏳ User management
399
+
400
+ ## Configuration Options
401
+
402
+ ### Document Options
403
+
404
+ | Option | Type | Description |
405
+ |--------|------|-------------|
406
+ | `name` | String | Document name (required) |
407
+ | `message` | String | Custom message for signers |
408
+ | `reminder` | String | Reminder frequency (`WEEKLY`, `DAILY`) |
409
+ | `sortable` | Boolean | Signers must sign in order |
410
+ | `footer` | String | Footer position (`BOTTOM`, `LEFT`, `RIGHT`) |
411
+ | `refusable` | Boolean | Allow document rejection |
412
+ | `qualified` | Boolean | Enable qualified signatures |
413
+ | `scrolling_required` | Boolean | Require full scroll before signing |
414
+ | `stop_on_rejected` | Boolean | Stop when document is rejected |
415
+ | `new_signature_style` | Boolean | Use new signature fields |
416
+ | `show_audit_page` | Boolean | Show audit page |
417
+ | `ignore_cpf` | Boolean | Don't require CPF |
418
+ | `ignore_birthdate` | Boolean | Don't require birthdate |
419
+ | `deadline_at` | DateTime | Signing deadline |
420
+
421
+ ### Signer Options
422
+
423
+ | Option | Type | Description |
424
+ |--------|------|-------------|
425
+ | `email` | String | Signer's email |
426
+ | `phone` | String | Signer's phone (for SMS/WhatsApp) |
427
+ | `name` | String | Signer's name (for link-based signing) |
428
+ | `action` | String | Action type (required) |
429
+ | `delivery_method` | String | Delivery method for phone signers |
430
+ | `configs` | Hash | Additional configs (e.g., CPF) |
431
+ | `security_verifications` | Array | Security checks |
432
+ | `positions` | Array | Signature field positions |
433
+
434
+ ## Rate Limiting
435
+
436
+ The Autentique API has a rate limit of **60 requests per minute**. The gem automatically handles rate limit errors.
437
+
438
+ ## Testing
439
+
440
+ ```bash
441
+ # Install dependencies
442
+ bundle install
443
+
444
+ # Run tests
445
+ bundle exec rspec
446
+
447
+ # Run with coverage
448
+ COVERAGE=true bundle exec rspec
449
+ ```
450
+
451
+ ## Development
452
+
453
+ ```bash
454
+ # Clone the repository
455
+ git clone https://github.com/yourusername/autentique-ruby.git
456
+ cd autentique-ruby
457
+
458
+ # Install dependencies
459
+ bundle install
460
+
461
+ # Run tests
462
+ bundle exec rspec
463
+
464
+ # Run console
465
+ bin/console
466
+
467
+ # Build gem
468
+ gem build autentique.gemspec
469
+
470
+ # Install locally
471
+ gem install autentique-0.1.0.gem
472
+ ```
473
+
474
+ ## Contributing
475
+
476
+ 1. Fork the repository
477
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
478
+ 3. Commit your changes (`git commit -am 'Add amazing feature'`)
479
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
480
+ 5. Open a Pull Request
481
+
482
+ ## Resources
483
+
484
+ - [Autentique Official Documentation](https://docs.autentique.com.br/api)
485
+ - [Autentique Dashboard](https://painel.autentique.com.br)
486
+ - [GraphQL Altair Explorer](https://altair.autentique.com.br)
487
+ - [API Keys](https://painel.autentique.com.br/perfil/api)
488
+
489
+ ## License
490
+
491
+ This gem is available as open source under the terms of the [MIT License](LICENSE).
492
+
493
+ ## Acknowledgments
494
+
495
+ This gem is not officially maintained by Autentique. It's a community-driven project to make integration easier for Ruby developers.
496
+
497
+ ## Support
498
+
499
+ - 🐛 Report bugs: [GitHub Issues](https://github.com/yourusername/autentique-ruby/issues)
500
+ - 💬 Questions: [GitHub Discussions](https://github.com/yourusername/autentique-ruby/discussions)
501
+
502
+ ## Changelog
503
+
504
+ See [CHANGELOG.md](CHANGELOG.md) for a list of changes.
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/autentique/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'autentique'
7
+ spec.version = Autentique::VERSION
8
+ spec.authors = ['Keith Yoder']
9
+ spec.email = ['keith.yoder@gmail.com']
10
+
11
+ spec.summary = 'Ruby client for Autentique digital signature API'
12
+ spec.description = 'A Ruby gem for integrating with Autentique\'s document signing service via their GraphQL API'
13
+ spec.homepage = 'https://github.com/keithyoder/autentique-ruby'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 2.7.0'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata['documentation_uri'] = 'https://docs.autentique.com.br/api'
21
+ spec.metadata['rubygems_mfa_required'] = 'true'
22
+
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ Dir['{lib}/**/*', '*.md', '*.gemspec', 'LICENSE*']
25
+ .reject { |f| File.directory?(f) }
26
+ end
27
+
28
+ spec.require_paths = ['lib']
29
+
30
+ # Runtime dependencies
31
+ spec.add_dependency 'graphql-client', '~> 0.18'
32
+ spec.add_dependency 'mime-types', '~> 3.0'
33
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphql/client'
4
+ require 'graphql/client/http'
5
+
6
+ module Autentique
7
+ class Client
8
+ API_ENDPOINT = 'https://api.autentique.com.br/v2/graphql'
9
+ SANDBOX_ENDPOINT = 'https://api.autentique.com.br/v2/graphql' # Same endpoint, uses sandbox param
10
+
11
+ attr_reader :api_key, :sandbox
12
+
13
+ # Initialize a new Autentique client
14
+ #
15
+ # @param api_key [String] Your Autentique API key
16
+ # @param sandbox [Boolean] Whether to use sandbox mode (default: false)
17
+ def initialize(api_key:, sandbox: false)
18
+ raise ArgumentError, 'Autentique API key is missing' if api_key.nil? || api_key.strip.empty?
19
+
20
+ @api_key = api_key
21
+ @sandbox = sandbox
22
+ @http_client = build_http_client
23
+ @graphql_client = build_graphql_client
24
+ end
25
+
26
+ # Access document-related operations
27
+ #
28
+ # @return [Autentique::Resources::Documents]
29
+ def documents
30
+ @documents ||= Resources::Documents.new(self)
31
+ end
32
+
33
+ # Access folder-related operations
34
+ #
35
+ # @return [Autentique::Resources::Folders]
36
+ def folders
37
+ @folders ||= Resources::Folders.new(self)
38
+ end
39
+
40
+ # Execute a GraphQL query
41
+ #
42
+ # @param query [GraphQL::Client::Query] The query to execute
43
+ # @param variables [Hash] Query variables
44
+ # @return [GraphQL::Client::Response]
45
+ def query(query, variables: {})
46
+ result = @graphql_client.query(query, variables: variables)
47
+
48
+ if result.errors.any?
49
+ messages = result.errors.map { |e| e['message'] }
50
+
51
+ if messages.any? { |m| m =~ /authentication/i }
52
+ raise AuthenticationError, 'Autentique API key is invalid or missing'
53
+ end
54
+
55
+ raise RateLimitError, 'Autentique API rate limit exceeded' if messages.any? { |m| m =~ /rate limit/i }
56
+
57
+ raise QueryError.new('GraphQL query failed', messages)
58
+ end
59
+
60
+ result
61
+ rescue Autentique::Error
62
+ # Re-raise any Autentique errors untouched
63
+ raise
64
+ rescue StandardError => e
65
+ # Wrap only unexpected low-level errors
66
+ raise Error, "Unexpected error in Autentique client: #{e.message}"
67
+ end
68
+
69
+ # Get the GraphQL client
70
+ #
71
+ # @return [GraphQL::Client]
72
+ attr_reader :graphql_client
73
+
74
+ private
75
+
76
+ def build_http_client
77
+ api_key = @api_key
78
+
79
+ GraphQL::Client::HTTP.new(API_ENDPOINT) do
80
+ define_method(:headers) do |_context|
81
+ { 'Authorization' => "Bearer #{api_key}" }
82
+ end
83
+ end
84
+ end
85
+
86
+ def build_graphql_client
87
+ begin
88
+ schema = GraphQL::Client.load_schema(@http_client)
89
+ rescue StandardError => e
90
+ raise Autentique::Error,
91
+ 'Unable to load GraphQL schema from Autentique API. ' \
92
+ 'Check your API key and network connectivity. ' \
93
+ "Original error: #{e.message}"
94
+ end
95
+
96
+ client = GraphQL::Client.new(schema: schema, execute: @http_client)
97
+ client.allow_dynamic_queries = true
98
+ client
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Autentique
4
+ # Base error class for all Autentique errors
5
+ class Error < StandardError; end
6
+
7
+ # Raised when API authentication fails
8
+ class AuthenticationError < Error; end
9
+
10
+ # Raised when API rate limit is exceeded
11
+ class RateLimitError < Error; end
12
+
13
+ # Raised when a resource is not found
14
+ class NotFoundError < Error; end
15
+
16
+ # Raised when validation fails
17
+ class ValidationError < Error; end
18
+
19
+ # Raised when file upload fails
20
+ class UploadError < Error; end
21
+
22
+ # Raised when GraphQL query fails
23
+ class QueryError < Error
24
+ attr_reader :errors
25
+
26
+ def initialize(message, errors = [])
27
+ super(message)
28
+ @errors = errors
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'document_input'
4
+ require_relative 'signer_input'
5
+ require_relative 'signature'
6
+
7
+ module Autentique
8
+ module Models
9
+ class Document
10
+ attr_reader :id, :name, :refusable, :sortable, :created_at, :signatures, :files
11
+
12
+ def initialize(attributes = {})
13
+ @id = attributes['id']
14
+ @name = attributes['name']
15
+ @refusable = attributes['refusable']
16
+ @sortable = attributes['sortable']
17
+ @created_at = attributes['created_at']
18
+ @signatures = parse_signatures(attributes['signatures'])
19
+ @files = attributes['files']
20
+ end
21
+
22
+ def signed?
23
+ signatures.all?(&:signed?)
24
+ end
25
+
26
+ def pending?
27
+ !signed?
28
+ end
29
+
30
+ def rejected?
31
+ signatures.any?(&:rejected?)
32
+ end
33
+
34
+ private
35
+
36
+ def parse_signatures(signatures_data)
37
+ return [] unless signatures_data
38
+
39
+ signatures_data.map { |sig| Signature.new(sig) }
40
+ end
41
+ end
42
+ end
43
+ end