assinafy 1.9.0 → 1.10.1

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: 6c202496157dd16d5ae51e1e6416ca30ab0d6ec3665ffa6dead88219214c82b8
4
- data.tar.gz: 9c07ecf32a3c9e9e94d964d56f6f38dc8c670d2cc011059d4c14409e79972b9e
3
+ metadata.gz: d8a12972c20d3f42452948edca772428326d37eafaf2354286bd9d3285bb6bf2
4
+ data.tar.gz: ad4b481584b05d052d7f44cde2a0cc7c99873e127031c30c60662c0a95dbca40
5
5
  SHA512:
6
- metadata.gz: 4bd51928265eb56100d62653932be2145442efb23e4708007070a027aeaff0e96678014c4d2a5b39ca2a9dcb37c6a13e5caf3b16798e28b6ad2ad535ef937068
7
- data.tar.gz: 7c1c6a6f07f0794b685b414195161f9832c74e9e196adff5f8713b26b44d7b94c188651184144af0b7393ad8bfea97d601a0e350c7165d2a4709356bcda0743a
6
+ metadata.gz: 8990224d37bac65328f95e7bdae28c999381f33fa64f1cfe300f3fffc124e8e98ab4e8e8473802534b1155ec06fc5c8080ffffb8a809118abf8d58cc661ad6a7
7
+ data.tar.gz: 35be652f65fa20a29cef34f3487f4af98ac9e94d613ec51c816b24ee879ed436b4c168aaeb51f32fef81864a9b7e54824812fa713693b2e7235e424ae407f085
data/CHANGELOG.md CHANGED
@@ -2,6 +2,20 @@
2
2
 
3
3
  All notable changes to the `assinafy` Ruby gem are documented here.
4
4
 
5
+ ## 1.10.1
6
+
7
+ - Update the pinned Ruby setup action to support Ruby 4.0.7 in CI and release workflows.
8
+
9
+ ## 1.10.0
10
+
11
+ - Support the RFC 8693 token-exchange grant for Assinafy-provisioned internal service clients.
12
+ - Validate OAuth grant parameters, PKCE challenges, authorization URLs, and token response shapes.
13
+ - Validate assignment deadlines, notification and verification method combinations, and signing steps before requests.
14
+ - Validate template role and signer IDs, distinct role assignments, and editor field payloads before document creation.
15
+ - Validate contact addresses, scalar payload fields, decline-reason lengths, and finite polling intervals consistently.
16
+ - Expand the document workflow guides, OAuth integration instructions, and API schema reference.
17
+ - Use Ruby 4.0.7 for the development and release toolchain.
18
+
5
19
  ## 1.9.0
6
20
 
7
21
  - `OAuthResource#refresh`, and `#token` with the `refresh_token` grant, raise `Assinafy::Error` instead of
data/README.en.md CHANGED
@@ -57,6 +57,7 @@ client = Assinafy::Client.new(
57
57
  )
58
58
 
59
59
  document = client.documents.upload({ file_path: './contract.pdf' })
60
+ document = client.documents.wait_until_ready(document.fetch('id'))
60
61
  signer = client.signers.create(full_name: 'Alice Silva', email: 'alice@example.com')
61
62
 
62
63
  assignment = client.assignments.create(
@@ -72,6 +73,7 @@ puts assignment['id']
72
73
  ## Configuration
73
74
 
74
75
  ```ruby
76
+ require 'logger'
75
77
  client = Assinafy::Client.new(
76
78
  api_key: 'your-api-key',
77
79
  token: nil,
@@ -270,6 +272,11 @@ client.oauth.authorization_server_metadata['code_challenge_methods_supported']
270
272
  `authorization_server_metadata` reaches a different host, so — like `/oauth/token` and `/oauth/revoke` — the SDK
271
273
  sends it with no workspace credentials attached.
272
274
 
275
+ **Internal service clients.** The advertised RFC 8693 token-exchange grant is restricted to
276
+ Assinafy-provisioned confidential service clients. Marketplace apps use PKCE authorization code
277
+ and refresh. `client.oauth.token` accepts token exchange with `client_secret`, `subject_token`,
278
+ `subject_token_type`, and `resource`; it issues no refresh token.
279
+
273
280
  **Errors.** OAuth routes answer with the flat RFC 6749 object rather than this API's envelope, so the SDK raises
274
281
  `Assinafy::OAuthError` (a subclass of `Assinafy::ApiError`):
275
282
 
@@ -343,11 +350,11 @@ client.documents.append_tags('document-id', ['tag-id-3'])
343
350
  # The deployed sandbox also accepts existing tag names in these arrays.
344
351
  client.documents.detach_tag('document-id', 'tag-id')
345
352
 
346
- # Template-driven creation
353
+ # Template-driven creation (a template with one configured Signer role)
347
354
  client.documents.create_from_template(
348
355
  'template-id',
349
356
  [{ role_id: 'role', id: 'signer-id', verification_method: 'Email', notification_methods: ['Email'] }],
350
- { name: 'Contract', message: 'Please sign', expires_at: '2026-12-31T23:59:00Z' }
357
+ { name: 'Contract', message: 'Please sign', expires_at: '2099-12-31T23:59:00Z' }
351
358
  )
352
359
  client.documents.estimate_cost_from_template(
353
360
  'template-id',
@@ -401,7 +408,7 @@ client.assignments.create(
401
408
  method: 'virtual',
402
409
  signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
403
410
  message: 'Please sign',
404
- expires_at: '2026-12-31T23:59:00Z',
411
+ expires_at: '2099-12-31T23:59:00Z',
405
412
  copy_receivers: ['cc-signer-id']
406
413
  )
407
414
 
@@ -417,7 +424,7 @@ client.assignments.create(
417
424
 
418
425
  client.assignments.list # GET /assignments (scoped to the account)
419
426
  client.assignments.estimate_cost('document-id', signers: [{ verification_method: 'Whatsapp' }])
420
- client.assignments.reset_expiration('document-id', 'assignment-id', '2026-12-31T23:59:00Z')
427
+ client.assignments.reset_expiration('document-id', 'assignment-id', '2099-12-31T23:59:00Z')
421
428
  client.assignments.reset_expiration('document-id', 'assignment-id', nil) # clears the expiry
422
429
  client.assignments.resend_notification('document-id', 'assignment-id', 'signer-id')
423
430
  client.assignments.estimate_resend_cost('document-id', 'assignment-id', 'signer-id')
@@ -436,6 +443,11 @@ client.assignments.decline('document-id', 'assignment-id', decline_reason: 'Clau
436
443
 
437
444
  > The `sign` request body is the API's camelCase body-key exception. This SDK accepts the snake_case keys (`item_id`, `field_id`, `page_id`, `value`) shown above and maps them to `itemId/fieldId/pageId/value` automatically. CamelCase input is also passed through unchanged. Assignment listing separately uses the live-required `accountId` query parameter.
438
445
 
446
+ Use exactly one notification channel per signer: `Email` verification pairs with `Email`,
447
+ `Whatsapp` pairs with `Whatsapp`, and `DigitalCertificate` permits either. If any signer has a
448
+ `step`, all must have one, with contiguous steps starting at 1. A certificate signer must be alone
449
+ in its step. Deadlines require a timezone and must be at least one hour in the future.
450
+
439
451
  ### Signer documents (multi-document workflows)
440
452
 
441
453
  ```ruby
@@ -459,7 +471,12 @@ client.templates.delete('template-id')
459
471
  client.templates.download_page('template-id', 'page-id') # binary image bytes
460
472
  ```
461
473
 
462
- > Template endpoints (`get`/`create`/`update`/`delete`/`download_page`) are live-verified against the sandbox but are not part of the current OpenAPI document. `create` requires a source file (`multipart/form-data`); the template name defaults to the uploaded file's name.
474
+ > Template endpoints (`get`/`create`/`update`/`delete`/`download_page`) are supported by the deployed API but are not part of the current OpenAPI document. `create` requires a source file (`multipart/form-data`); the template name defaults to the uploaded file's name.
475
+
476
+ A template upload initially contains an `Editor` role. Configure at least one `Signer` role and its
477
+ fields in the Assinafy application before generating a document. Supply one entry per role, each with
478
+ a distinct existing signer ID; `role_id` comes from the template's `roles` array. Cost estimation
479
+ requires `role_id` but can omit the signer ID.
463
480
 
464
481
  ### Tags
465
482
 
@@ -639,16 +656,18 @@ assignment = client.assignments.create(
639
656
  method: 'virtual',
640
657
  signers: [signer_request],
641
658
  message: 'Please review and sign.',
642
- expires_at: '2026-12-31T23:59:00Z'
659
+ expires_at: '2099-12-31T23:59:00Z'
643
660
  )
644
661
  # => Assignment, including signing_urls
645
662
  ```
646
663
 
647
- The template alternative binds a signer to a template role and creates the document and assignment together:
664
+ For a template configured with exactly one `Signer` role, the alternative binds the existing signer
665
+ to that role and creates the document and assignment together. Templates with more roles require
666
+ a distinct signer entry for each role:
648
667
 
649
668
  ```ruby
650
669
  role_signer = {
651
- role_id: template.fetch('roles').first.fetch('id'),
670
+ role_id: template.fetch('roles').find { |role| role['assignment_type'] == 'Signer' }.fetch('id'),
652
671
  id: signer.fetch('id'),
653
672
  verification_method: 'Email',
654
673
  notification_methods: ['Email']
@@ -721,7 +740,8 @@ signer is created.
721
740
  Requesting the method is fully supported. Completing the signature is not: the two-step handshake
722
741
  (`/signers/certificate/start`, `/signers/certificate/complete`) is absent from the API v1 machine contract — its
723
742
  authentication and request/response schemas are not published — so the SDK does not expose those completion
724
- calls rather than guess at their payloads. Contact Assinafy before enabling a digital-certificate signing flow.
743
+ calls. Direct A1/A3 signers to Assinafy's hosted signing flow using the returned `signing_urls`;
744
+ contact Assinafy for a published contract before integrating the handshake directly.
725
745
  Once it completes, the `pades` artifact returns the qualified signature.
726
746
 
727
747
  ### 7. Inspect, tag, download, and verify
@@ -734,7 +754,8 @@ tags = client.documents.append_tags(document_id, ['tag-id']) # => Array<Tag>
734
754
  client.documents.replace_tags(document_id, tags.map { |tag| tag.fetch('id') })
735
755
 
736
756
  original_pdf = client.documents.download(document_id, 'original')
737
- signed_pdf = client.documents.download(document_id, 'pades')
757
+ signed_pdf = client.documents.download(document_id, 'certificated')
758
+ # For documents completed with ICP-Brasil certificates: download(document_id, 'pades')
738
759
  certificate_pdf = client.documents.download(document_id, 'certificated')
739
760
 
740
761
  verification = client.documents.verify('signature-hash-from-assinafy')
@@ -787,10 +808,10 @@ result = client.upload_and_request_signatures(
787
808
  source: { file_path: './contract.pdf' },
788
809
  signers: [
789
810
  { full_name: 'Alice Silva', email: 'alice@example.com' },
790
- { full_name: 'Bob Costa', whatsapp_phone_number: '+5548999990000' }
811
+ { full_name: 'Bob Costa', email: 'bob@example.com' }
791
812
  ],
792
813
  message: 'Please sign.',
793
- expires_at: '2026-12-31T23:59:00Z'
814
+ expires_at: '2099-12-31T23:59:00Z'
794
815
  )
795
816
 
796
817
  puts result[:document]['id']
@@ -841,10 +862,16 @@ ASSINAFY_API_KEY=... \
841
862
  ASSINAFY_ACCOUNT_ID=... \
842
863
  ASSINAFY_TEST_EMAIL=recipient1@example.com \
843
864
  ASSINAFY_TEST_EMAIL2=recipient2@example.com \
865
+ ASSINAFY_TEMPLATE_ID=configured-template-id \
844
866
  ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
845
867
  bundle exec rspec spec/integration
846
868
  ```
847
869
 
870
+ `ASSINAFY_TEMPLATE_ID` must identify a sandbox template with a `Signer` role configured in the
871
+ application. Without it, the positive template-generation example is skipped. Supply different test
872
+ recipients for its roles. OAuth browser consent is tested separately with a registered app, an HTTPS
873
+ callback, and the matching authorization server; sandbox keys are not production OAuth credentials.
874
+
848
875
  > These tests create and clean up real resources and, for the assignment flow, send real signature-request emails to the addresses in `ASSINAFY_TEST_EMAIL` / `ASSINAFY_TEST_EMAIL2`.
849
876
 
850
877
  ## Contributing
data/README.md CHANGED
@@ -83,6 +83,7 @@ A Assinafy aceita três credenciais. Escolha pela pergunta "**quem** está agind
83
83
 
84
84
  ```ruby
85
85
  require 'assinafy'
86
+ require 'logger'
86
87
 
87
88
  client = Assinafy::Client.new(
88
89
  api_key: ENV.fetch('ASSINAFY_API_KEY'),
@@ -127,7 +128,7 @@ O caminho mais curto do PDF até o pedido de assinatura:
127
128
  ```ruby
128
129
  resultado = client.upload_and_request_signatures(
129
130
  source: './contrato.pdf',
130
- signers: [{ full_name: 'Ana Silva', email: 'ana@exemplo.com.br' }],
131
+ signers: [{ full_name: 'Ana Silva', email: 'ana@example.com' }],
131
132
  message: 'Por favor, assine o contrato em anexo.'
132
133
  )
133
134
 
@@ -192,18 +193,20 @@ Erros de rede durante a espera são tolerados e reprocessados; um status termina
192
193
  ```ruby
193
194
  signatario = client.signers.create(
194
195
  full_name: 'Ana Silva',
195
- email: 'ana@exemplo.com.br',
196
- whatsapp_phone_number: '+5511999999999', # obrigatório para verificação por WhatsApp
197
- government_id: '00000000000' # obrigatório para certificado digital
196
+ email: 'ana@example.com',
197
+ whatsapp_phone_number: '+5500000000000' # substitua por um número de teste válido
198
198
  )
199
199
  signatario['id'] # => "19e6b92e7895332ed9708535d8c"
200
+
201
+ # Para certificado digital, cadastre o CPF/CNPJ real autorizado antes do assignment:
202
+ client.signers.update(signatario['id'], government_id: ENV.fetch('ASSINAFY_SIGNER_GOVERNMENT_ID'))
200
203
  ```
201
204
 
202
205
  Signatários pertencem à conta e podem ser reaproveitados entre documentos:
203
206
 
204
207
  ```ruby
205
- existente = client.signers.find_by_email('ana@exemplo.com.br') # paginação percorrida pelo SDK
206
- signatario = existente || client.signers.create(full_name: 'Ana Silva', email: 'ana@exemplo.com.br')
208
+ existente = client.signers.find_by_email('ana@example.com') # paginação percorrida pelo SDK
209
+ signatario = existente || client.signers.create(full_name: 'Ana Silva', email: 'ana@example.com')
207
210
  ```
208
211
 
209
212
  ### 4.4 Estimar o custo (opcional, recomendado)
@@ -233,7 +236,7 @@ assignment = client.assignments.create(
233
236
  { id: signatario['id'], verification_method: 'Email', notification_methods: ['Email'], step: 1 }
234
237
  ],
235
238
  message: 'Por favor, assine o contrato em anexo.',
236
- expires_at: '2026-12-31T23:59:00Z',
239
+ expires_at: '2099-12-31T23:59:00Z',
237
240
  copy_receivers: [] # IDs que só recebem cópia
238
241
  )
239
242
 
@@ -241,8 +244,9 @@ assignment['signing_urls']
241
244
  # => [{ 'signer_id' => '19e6b...', 'url' => 'https://.../sign/...' }]
242
245
  ```
243
246
 
244
- `step` define a ordem: todos do passo 1 assinam antes do passo 2. Omita para assinatura
245
- simultânea.
247
+ `step` define a ordem: todos do passo 1 assinam antes do passo 2. Se informado para um
248
+ signatário, informe para todos, em uma sequência contínua iniciada em 1. Omita para assinatura
249
+ simultânea. `expires_at` deve ter fuso horário e estar pelo menos uma hora no futuro.
246
250
 
247
251
  **`collect`** — campos posicionados página a página.
248
252
 
@@ -349,7 +353,7 @@ client.assignments.resend_notification(documento['id'], assignment['id'], signat
349
353
  Estender o prazo:
350
354
 
351
355
  ```ruby
352
- client.assignments.reset_expiration(documento['id'], assignment['id'], '2027-01-31T23:59:00Z')
356
+ client.assignments.reset_expiration(documento['id'], assignment['id'], '2099-01-31T23:59:00Z')
353
357
  ```
354
358
 
355
359
  Para acompanhar sem polling, use [webhooks](#8-webhooks).
@@ -417,7 +421,7 @@ session[:assinafy_issuer] = Assinafy::OAuth::AUTHORIZATION_SERVER # emiss
417
421
 
418
422
  redirect_to Assinafy::OAuth.authorization_url(
419
423
  client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
420
- redirect_uri: 'https://app.exemplo.com.br/oauth/callback',
424
+ redirect_uri: 'https://app.example.com/oauth/callback',
421
425
  code_verifier: verificador,
422
426
  scope: %w[documents:read documents:write offline_access],
423
427
  state: state
@@ -442,7 +446,7 @@ tokens = Assinafy::Client.new.oauth.exchange_code(
442
446
  code: params.fetch(:code),
443
447
  client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
444
448
  code_verifier: session.delete(:assinafy_code_verifier),
445
- redirect_uri: 'https://app.exemplo.com.br/oauth/callback'
449
+ redirect_uri: 'https://app.example.com/oauth/callback'
446
450
  )
447
451
 
448
452
  tokens['access_token'] # => "..."
@@ -533,6 +537,13 @@ servidor['token_endpoint'] # => "https://api.assinafy.com.br/
533
537
  servidor['code_challenge_methods_supported'] # => ["S256"]
534
538
  ```
535
539
 
540
+ ### Clientes internos de serviço
541
+
542
+ O servidor também publica o grant RFC 8693 `urn:ietf:params:oauth:grant-type:token-exchange`.
543
+ Ele é reservado a clientes confidenciais internos provisionados pela Assinafy; aplicativos de
544
+ marketplace usam autorização com PKCE e renovação. O método `client.oauth.token` aceita esse grant,
545
+ com `client_secret`, `subject_token`, `subject_token_type` e `resource`. Ele não emite refresh token.
546
+
536
547
  ### Erros OAuth
537
548
 
538
549
  Os endpoints OAuth respondem com o objeto plano da RFC 6749, não com o envelope da API. O SDK
@@ -572,7 +583,7 @@ Os valores aceitos estão publicados em
572
583
  valida localmente: um valor fora do enum levanta `ValidationError` **antes** de a requisição
573
584
  sair — e portanto antes de qualquer signatário ser criado para aquele assignment.
574
585
 
575
- Combinações permitidas: `Email` → notifica por `Email`; `Whatsapp` → notifica por `Whatsapp`;
586
+ Informe exatamente um canal em `notification_methods`. Combinações permitidas: `Email` → notifica por `Email`; `Whatsapp` → notifica por `Whatsapp`;
576
587
  `DigitalCertificate` → notifica por `Email` **ou** `Whatsapp`.
577
588
 
578
589
  ### Certificado digital ICP-Brasil (A1/A3)
@@ -590,7 +601,7 @@ client.assignments.create(
590
601
  )
591
602
  ```
592
603
 
593
- Antes de abrir o assignment, o signatário precisa confirmar os dados de identidade e aceitar os
604
+ Antes de concluir a assinatura, o signatário precisa confirmar os dados de identidade e aceitar os
594
605
  termos. O endpoint comum de assinatura **rejeita** signatários por certificado — a assinatura
595
606
  deles é produzida por um handshake de dois passos com a extensão Web PKI:
596
607
 
@@ -603,7 +614,8 @@ POST /v1/signers/certificate/complete → data.signerName
603
614
  > Essas duas rotas são extensões que **não constam do documento OpenAPI publicado**: a
604
615
  > autenticação e os esquemas de requisição/resposta delas não são documentados. Como envolveria
605
616
  > adivinhar o payload, o SDK **não** expõe essas duas chamadas — fale com a Assinafy antes de
606
- > habilitar o fluxo em produção.
617
+ > integrar essas rotas diretamente. Para A1/A3, direcione o signatário ao fluxo hospedado
618
+ > de assinatura da Assinafy usando `assignment['signing_urls']`.
607
619
 
608
620
  Concluído o fluxo, baixar o artefato `pades` devolve a assinatura PAdES qualificada.
609
621
 
@@ -624,15 +636,19 @@ client.templates.download_page(template['id'], pagina_id)
624
636
  client.templates.delete(template['id'])
625
637
  ```
626
638
 
627
- Gerar um documento a partir de um template:
639
+ Configure os papéis e campos no aplicativo Assinafy antes de gerar documentos. O upload cria
640
+ um papel `Editor`; a geração exige pelo menos um papel `Signer`. Use um signatário já existente
641
+ e diferente para cada papel. `role_id` vem de `template['roles']`, e `id` é o ID do signatário.
642
+
643
+ Gerar um documento a partir de um template com um único papel `Signer`:
628
644
 
629
645
  ```ruby
630
646
  client.documents.estimate_cost_from_template(
631
- template['id'], [{ full_name: 'Ana Silva', email: 'ana@exemplo.com.br' }]
647
+ template['id'], [{ role_id: 'papel-assinante-id', id: signatario['id'] }]
632
648
  )
633
649
 
634
650
  documento = client.documents.create_from_template(
635
- template['id'], [{ full_name: 'Ana Silva', email: 'ana@exemplo.com.br' }]
651
+ template['id'], [{ role_id: 'papel-assinante-id', id: signatario['id'] }]
636
652
  )
637
653
  ```
638
654
 
@@ -641,7 +657,7 @@ documento = client.documents.create_from_template(
641
657
  Definições de campo reutilizáveis, com validação opcional por regex:
642
658
 
643
659
  ```ruby
644
- campo = client.fields.create(name: 'CPF', type: 'text', regex: '\A\d{11}\z')
660
+ campo = client.fields.create(name: 'CPF', type: 'text', regex: '/^\d{11}$/')
645
661
  client.fields.types # tipos de campo disponíveis
646
662
  client.fields.list
647
663
  client.fields.validate(campo['id'], '12345678901')
@@ -673,9 +689,9 @@ Registrar a assinatura de eventos da conta:
673
689
  client.webhooks.list_event_types # eventos disponíveis
674
690
 
675
691
  client.webhooks.register(
676
- url: 'https://app.exemplo.com.br/webhooks/assinafy',
677
- email: 'ops@exemplo.com.br', # para avisos de falha de entrega
678
- events: %w[document.completed signer.declined],
692
+ url: 'https://app.example.com/webhooks/assinafy',
693
+ email: 'ops@example.com', # para avisos de falha de entrega
694
+ events: %w[document_ready document_prepared signer_signed_document],
679
695
  is_active: true
680
696
  )
681
697
 
@@ -685,20 +701,22 @@ client.webhooks.list_dispatches
685
701
  client.webhooks.retry_dispatch(dispatch_id)
686
702
  ```
687
703
 
688
- Verificar a assinatura HMAC-SHA256 do payload recebido:
704
+ A API não publica um esquema de assinatura para os webhooks enviados. O verificador HMAC-SHA256
705
+ local é opcional: use-o quando um gateway seu assina o corpo bruto com um segredo compartilhado.
706
+ O cabeçalho abaixo é inserido por esse gateway, e não pela Assinafy:
689
707
 
690
708
  ```ruby
691
709
  verificador = client.webhook_verifier # usa o webhook_secret do cliente
692
710
 
693
711
  post '/webhooks/assinafy' do
694
712
  corpo = request.body.read
695
- assinatura = request.env['HTTP_X_ASSINAFY_SIGNATURE']
713
+ assinatura = request.env['HTTP_X_WEBHOOK_SIGNATURE']
696
714
 
697
715
  halt 401 unless verificador.verify(corpo, assinatura)
698
716
 
699
717
  evento = verificador.extract_event(corpo)
700
718
  case verificador.event_type(evento)
701
- when 'document.completed' then processar(verificador.event_data(evento))
719
+ when 'signer_signed_document' then processar(verificador.event_object(evento))
702
720
  end
703
721
 
704
722
  200
@@ -853,6 +871,20 @@ método novo chega à gem sem uma.
853
871
  - [CHANGELOG.md](CHANGELOG.md) — histórico de versões
854
872
  - [Documentação da API](https://api.assinafy.com.br/v1/docs)
855
873
 
874
+ ## Desenvolvimento e testes
875
+
876
+ ```bash
877
+ bundle exec rake spec
878
+ bundle exec rubocop
879
+ bundle exec bundler-audit check --update
880
+ rbs -I sig validate
881
+ ruby scripts/check_api_contract.rb
882
+ ```
883
+
884
+ A [suíte sandbox](spec/integration/live_sandbox_spec.rb) fica fora da execução padrão.
885
+ Veja os [pré-requisitos e variáveis](README.en.md#live-integration-tests), incluindo um template
886
+ com papel `Signer` para testar a geração. Os testes enviam e-mails e removem os recursos criados.
887
+
856
888
  ## Licença
857
889
 
858
890
  Distribuído sob a licença [MIT](LICENSE).