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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +62 -0
- data/CONTRIBUTING.md +254 -0
- data/LICENSE +21 -0
- data/README.md +504 -0
- data/autentique.gemspec +33 -0
- data/lib/autentique/client.rb +101 -0
- data/lib/autentique/errors.rb +31 -0
- data/lib/autentique/models/document.rb +43 -0
- data/lib/autentique/models/document_input.rb +43 -0
- data/lib/autentique/models/signature.rb +41 -0
- data/lib/autentique/models/signer_input.rb +29 -0
- data/lib/autentique/resources/documents/create.rb +33 -0
- data/lib/autentique/resources/documents/delete.rb +26 -0
- data/lib/autentique/resources/documents/find.rb +45 -0
- data/lib/autentique/resources/documents/list.rb +41 -0
- data/lib/autentique/resources/documents/pending.rb +40 -0
- data/lib/autentique/resources/documents.rb +164 -0
- data/lib/autentique/resources/folders.rb +78 -0
- data/lib/autentique/resources.rb +6 -0
- data/lib/autentique/version.rb +5 -0
- data/lib/autentique.rb +74 -0
- metadata +98 -0
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
|
+
[](https://github.com/keithyoder/autentique-ruby/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/keithyoder/autentique-ruby/actions/workflows/security.yml)
|
|
7
|
+
[](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.
|
data/autentique.gemspec
ADDED
|
@@ -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
|