assinafy 1.8.1 → 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 +4 -4
- data/CHANGELOG.md +33 -0
- data/README.en.md +103 -33
- data/README.md +120 -43
- data/docs/API_REFERENCE.md +142 -43
- data/lib/assinafy/client.rb +4 -0
- data/lib/assinafy/oauth.rb +42 -21
- data/lib/assinafy/resources/account_resource.rb +42 -1
- data/lib/assinafy/resources/assignment_resource.rb +92 -25
- data/lib/assinafy/resources/auth_resource.rb +55 -1
- data/lib/assinafy/resources/base_resource.rb +42 -8
- data/lib/assinafy/resources/document_resource.rb +101 -13
- data/lib/assinafy/resources/field_resource.rb +29 -0
- data/lib/assinafy/resources/oauth_resource.rb +150 -52
- data/lib/assinafy/resources/signer_document_resource.rb +20 -1
- data/lib/assinafy/resources/signer_resource.rb +37 -6
- data/lib/assinafy/resources/tag_resource.rb +12 -1
- data/lib/assinafy/resources/template_resource.rb +16 -0
- data/lib/assinafy/resources/user_resource.rb +12 -0
- data/lib/assinafy/resources/webhook_resource.rb +20 -2
- data/lib/assinafy/utils.rb +38 -0
- data/lib/assinafy/version.rb +1 -1
- data/sig/assinafy.rbs +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8a12972c20d3f42452948edca772428326d37eafaf2354286bd9d3285bb6bf2
|
|
4
|
+
data.tar.gz: ad4b481584b05d052d7f44cde2a0cc7c99873e127031c30c60662c0a95dbca40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8990224d37bac65328f95e7bdae28c999381f33fa64f1cfe300f3fffc124e8e98ab4e8e8473802534b1155ec06fc5c8080ffffb8a809118abf8d58cc661ad6a7
|
|
7
|
+
data.tar.gz: 35be652f65fa20a29cef34f3487f4af98ac9e94d613ec51c816b24ee879ed436b4c168aaeb51f32fef81864a9b7e54824812fa713693b2e7235e424ae407f085
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,39 @@
|
|
|
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
|
+
|
|
19
|
+
## 1.9.0
|
|
20
|
+
|
|
21
|
+
- `OAuthResource#refresh`, and `#token` with the `refresh_token` grant, raise `Assinafy::Error` instead of
|
|
22
|
+
returning a success whose `refresh_token` is missing, blank, or the one sent. The token sent may already be
|
|
23
|
+
retired, so handle the error like `invalid_grant`.
|
|
24
|
+
- `OAuthResource` posts `/oauth/token` and `/oauth/revoke` bodies as `application/x-www-form-urlencoded`, as
|
|
25
|
+
RFC 6749 and RFC 7009 define them. The API accepts this and JSON alike; no caller change is needed.
|
|
26
|
+
- Every `ApiError`, not only `OAuthError`, carries the `WWW-Authenticate` challenge in
|
|
27
|
+
`context[:www_authenticate]`, including one raised for an error status inside a `2xx` envelope, so a `403` for
|
|
28
|
+
a missing OAuth scope can be told apart from one for another workspace or role.
|
|
29
|
+
- `DocumentResource#verify` documents the nullable `agreement_code` field, the agreement code printed on the
|
|
30
|
+
document certificate. `spec/fixtures/api_contract.json` tracks the current contract.
|
|
31
|
+
- `scripts/check_api_contract.rb` requires TLS 1.2 or newer.
|
|
32
|
+
- The OAuth guides cover refresh-token rotation (every refresh returns a new refresh token valid for 30 days and
|
|
33
|
+
retires the old one): storing the new tokens and rebuilding the client, never resending a refresh token after
|
|
34
|
+
an ambiguous failure, and revoking the refresh token currently stored on disconnect. They also cover checking
|
|
35
|
+
`state` and `iss` against the values stored for each authorization attempt (the sandbox has its own issuer)
|
|
36
|
+
and finding the connected workspace through `GET /accounts`.
|
|
37
|
+
|
|
5
38
|
## 1.8.1
|
|
6
39
|
|
|
7
40
|
- The SDK's own HTTPS client now requires TLS 1.2 or newer; TLS 1.0 and 1.1 are refused. Caller-supplied clients are unchanged.
|
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,
|
|
@@ -154,7 +156,7 @@ The flow is authorization-code with **mandatory PKCE**; the authorization server
|
|
|
154
156
|
| `email` | Include the user's email and verification status in the claims |
|
|
155
157
|
| `offline_access` | Be issued a refresh token |
|
|
156
158
|
|
|
157
|
-
**1. Redirect the user.** Store the verifier
|
|
159
|
+
**1. Redirect the user.** Store the verifier, the state, and the expected issuer in the session first.
|
|
158
160
|
|
|
159
161
|
```ruby
|
|
160
162
|
verifier = Assinafy::OAuth.generate_code_verifier
|
|
@@ -162,6 +164,7 @@ state = Assinafy::OAuth.generate_state
|
|
|
162
164
|
|
|
163
165
|
session[:assinafy_code_verifier] = verifier
|
|
164
166
|
session[:assinafy_state] = state
|
|
167
|
+
session[:assinafy_issuer] = Assinafy::OAuth::AUTHORIZATION_SERVER # issuer of the server this attempt uses
|
|
165
168
|
|
|
166
169
|
redirect_to Assinafy::OAuth.authorization_url(
|
|
167
170
|
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
@@ -173,11 +176,20 @@ redirect_to Assinafy::OAuth.authorization_url(
|
|
|
173
176
|
```
|
|
174
177
|
|
|
175
178
|
The `code_challenge` is derived from the verifier; the verifier itself never appears in the URL.
|
|
179
|
+
`Assinafy::OAuth::AUTHORIZATION_SERVER` is the production issuer. The sandbox has its own: pass
|
|
180
|
+
`authorization_endpoint: 'https://auth-sandbox.assinafy.com.br/oauth/authorize'` and store
|
|
181
|
+
`https://auth-sandbox.assinafy.com.br` instead.
|
|
176
182
|
|
|
177
|
-
**2. Exchange the code.**
|
|
183
|
+
**2. Exchange the code.** Check `state` and `iss` against the values stored for this attempt before anything
|
|
184
|
+
else, including on an `error=` return — that is the CSRF check, and it rejects responses that are not yours. The
|
|
185
|
+
code is single-use and expires 60 seconds after approval: exchange it at once, and never retry.
|
|
178
186
|
|
|
179
187
|
```ruby
|
|
180
|
-
|
|
188
|
+
unless params[:state] == session.delete(:assinafy_state) &&
|
|
189
|
+
params[:iss] == session.delete(:assinafy_issuer)
|
|
190
|
+
raise 'authorization response is not ours'
|
|
191
|
+
end
|
|
192
|
+
raise "authorization not granted: #{params[:error]}" if params[:error] # access_denied, invalid_scope, ...
|
|
181
193
|
|
|
182
194
|
tokens = Assinafy::Client.new.oauth.exchange_code(
|
|
183
195
|
code: params.fetch(:code),
|
|
@@ -189,34 +201,63 @@ tokens = Assinafy::Client.new.oauth.exchange_code(
|
|
|
189
201
|
# 'refresh_token' => ..., 'scope' => 'documents:read documents:write' }
|
|
190
202
|
```
|
|
191
203
|
|
|
192
|
-
**3. Act as the user.**
|
|
204
|
+
**3. Act as the user.** The token works only in the workspace the user picked; store its id next to the tokens.
|
|
193
205
|
|
|
194
206
|
```ruby
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
account_id: ENV.fetch('ASSINAFY_ACCOUNT_ID')
|
|
198
|
-
)
|
|
207
|
+
access_token = tokens.fetch('access_token')
|
|
208
|
+
workspace_id = Assinafy::Client.new(token: access_token).accounts.list[:data].first.fetch('id')
|
|
199
209
|
|
|
210
|
+
user_client = Assinafy::Client.new(token: access_token, account_id: workspace_id)
|
|
200
211
|
user_client.documents.list
|
|
201
212
|
user_client.oauth.userinfo # => { 'sub' => ..., 'name' => ..., 'email' => ... }
|
|
202
213
|
```
|
|
203
214
|
|
|
204
|
-
**Refresh and revoke.**
|
|
215
|
+
**Refresh and revoke.** `connection` below is your stored record for this user: tokens, expiry, workspace id.
|
|
205
216
|
|
|
206
217
|
```ruby
|
|
207
|
-
|
|
218
|
+
tokens = Assinafy::Client.new.oauth.refresh(
|
|
219
|
+
refresh_token: connection.refresh_token,
|
|
220
|
+
client_id: ENV.fetch('ASSINAFY_CLIENT_ID')
|
|
221
|
+
)
|
|
208
222
|
|
|
209
|
-
|
|
210
|
-
|
|
223
|
+
# The refresh token just sent is retired: store the new tokens before anything else.
|
|
224
|
+
connection.update!(
|
|
225
|
+
refresh_token: tokens.fetch('refresh_token'),
|
|
226
|
+
access_token: tokens.fetch('access_token'),
|
|
227
|
+
expires_at: Time.now + tokens.fetch('expires_in')
|
|
211
228
|
)
|
|
229
|
+
user_client = Assinafy::Client.new(token: connection.access_token, account_id: connection.workspace_id)
|
|
212
230
|
```
|
|
213
231
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
already-revoked token — so the endpoint cannot be used to probe whether a token exists.
|
|
232
|
+
When a user disconnects, revoke the refresh token in storage **now** — the latest one — then delete the stored
|
|
233
|
+
tokens:
|
|
217
234
|
|
|
218
|
-
|
|
219
|
-
|
|
235
|
+
```ruby
|
|
236
|
+
Assinafy::Client.new.oauth.revoke(
|
|
237
|
+
token: connection.reload.refresh_token,
|
|
238
|
+
client_id: ENV.fetch('ASSINAFY_CLIENT_ID'),
|
|
239
|
+
token_type_hint: 'refresh_token'
|
|
240
|
+
)
|
|
241
|
+
connection.destroy!
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
A refresh token exists only when `offline_access` was requested *and* consented. Revoking a refresh token also
|
|
245
|
+
invalidates the access tokens issued from it. Every revoke outcome returns `200` — including an unknown,
|
|
246
|
+
already-revoked, or rotated token — so the endpoint cannot be used to probe whether a token exists, and revoking
|
|
247
|
+
a stale copy can look successful while the connection stays active.
|
|
248
|
+
|
|
249
|
+
> The SDK does **not** refresh automatically. Persist `expires_in` and refresh before expiry. Every refresh
|
|
250
|
+
> returns a **new** refresh token, valid for another 30 days, and retires the old one, so a connection only
|
|
251
|
+
> expires after 30 days without a refresh. Reusing a retired refresh token ends the whole connection: store the
|
|
252
|
+
> new tokens before using them, and refresh one at a time per connection. `refresh` raises `Assinafy::Error`
|
|
253
|
+
> rather than return a success without a new refresh token; handle it like `invalid_grant`.
|
|
254
|
+
>
|
|
255
|
+
> The SDK sends each token request once. If a refresh fails without a clear answer — a timeout, a reset
|
|
256
|
+
> connection, a `5xx` — the server may have rotated the token without the response reaching you. Re-read the
|
|
257
|
+
> stored refresh token: if it is still the one you sent, **never send it again**; ask the user to connect again.
|
|
258
|
+
> Carry on only if another worker has since stored a different one. Only a failure that provably happened before
|
|
259
|
+
> the request was sent — DNS resolution, a refused connection, a failed TLS handshake — is safe to retry. On an
|
|
260
|
+
> API `401`, refresh once; if that fails, or on `invalid_grant`, ask the user to connect again.
|
|
220
261
|
|
|
221
262
|
**Discovery**, instead of hardcoding endpoints:
|
|
222
263
|
|
|
@@ -231,6 +272,11 @@ client.oauth.authorization_server_metadata['code_challenge_methods_supported']
|
|
|
231
272
|
`authorization_server_metadata` reaches a different host, so — like `/oauth/token` and `/oauth/revoke` — the SDK
|
|
232
273
|
sends it with no workspace credentials attached.
|
|
233
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
|
+
|
|
234
280
|
**Errors.** OAuth routes answer with the flat RFC 6749 object rather than this API's envelope, so the SDK raises
|
|
235
281
|
`Assinafy::OAuthError` (a subclass of `Assinafy::ApiError`):
|
|
236
282
|
|
|
@@ -241,6 +287,10 @@ rescue Assinafy::OAuthError => e
|
|
|
241
287
|
e.context[:www_authenticate] # on a 403, names the missing scope
|
|
242
288
|
```
|
|
243
289
|
|
|
290
|
+
Any resource, not only these routes, puts that challenge in `context[:www_authenticate]`. Treat it as a prompt to
|
|
291
|
+
reconnect with that scope added, not as a retry; a `403` without it means another workspace, the user's role, or
|
|
292
|
+
an area OAuth tokens never reach.
|
|
293
|
+
|
|
244
294
|
### Accounts
|
|
245
295
|
|
|
246
296
|
```ruby
|
|
@@ -300,11 +350,11 @@ client.documents.append_tags('document-id', ['tag-id-3'])
|
|
|
300
350
|
# The deployed sandbox also accepts existing tag names in these arrays.
|
|
301
351
|
client.documents.detach_tag('document-id', 'tag-id')
|
|
302
352
|
|
|
303
|
-
# Template-driven creation
|
|
353
|
+
# Template-driven creation (a template with one configured Signer role)
|
|
304
354
|
client.documents.create_from_template(
|
|
305
355
|
'template-id',
|
|
306
356
|
[{ role_id: 'role', id: 'signer-id', verification_method: 'Email', notification_methods: ['Email'] }],
|
|
307
|
-
{ name: 'Contract', message: 'Please sign', expires_at: '
|
|
357
|
+
{ name: 'Contract', message: 'Please sign', expires_at: '2099-12-31T23:59:00Z' }
|
|
308
358
|
)
|
|
309
359
|
client.documents.estimate_cost_from_template(
|
|
310
360
|
'template-id',
|
|
@@ -358,7 +408,7 @@ client.assignments.create(
|
|
|
358
408
|
method: 'virtual',
|
|
359
409
|
signers: [{ id: 'signer-1', verification_method: 'Email', notification_methods: ['Email'], step: 1 }],
|
|
360
410
|
message: 'Please sign',
|
|
361
|
-
expires_at: '
|
|
411
|
+
expires_at: '2099-12-31T23:59:00Z',
|
|
362
412
|
copy_receivers: ['cc-signer-id']
|
|
363
413
|
)
|
|
364
414
|
|
|
@@ -374,7 +424,7 @@ client.assignments.create(
|
|
|
374
424
|
|
|
375
425
|
client.assignments.list # GET /assignments (scoped to the account)
|
|
376
426
|
client.assignments.estimate_cost('document-id', signers: [{ verification_method: 'Whatsapp' }])
|
|
377
|
-
client.assignments.reset_expiration('document-id', 'assignment-id', '
|
|
427
|
+
client.assignments.reset_expiration('document-id', 'assignment-id', '2099-12-31T23:59:00Z')
|
|
378
428
|
client.assignments.reset_expiration('document-id', 'assignment-id', nil) # clears the expiry
|
|
379
429
|
client.assignments.resend_notification('document-id', 'assignment-id', 'signer-id')
|
|
380
430
|
client.assignments.estimate_resend_cost('document-id', 'assignment-id', 'signer-id')
|
|
@@ -393,6 +443,11 @@ client.assignments.decline('document-id', 'assignment-id', decline_reason: 'Clau
|
|
|
393
443
|
|
|
394
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.
|
|
395
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
|
+
|
|
396
451
|
### Signer documents (multi-document workflows)
|
|
397
452
|
|
|
398
453
|
```ruby
|
|
@@ -416,7 +471,12 @@ client.templates.delete('template-id')
|
|
|
416
471
|
client.templates.download_page('template-id', 'page-id') # binary image bytes
|
|
417
472
|
```
|
|
418
473
|
|
|
419
|
-
> Template endpoints (`get`/`create`/`update`/`delete`/`download_page`) are
|
|
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.
|
|
420
480
|
|
|
421
481
|
### Tags
|
|
422
482
|
|
|
@@ -596,16 +656,18 @@ assignment = client.assignments.create(
|
|
|
596
656
|
method: 'virtual',
|
|
597
657
|
signers: [signer_request],
|
|
598
658
|
message: 'Please review and sign.',
|
|
599
|
-
expires_at: '
|
|
659
|
+
expires_at: '2099-12-31T23:59:00Z'
|
|
600
660
|
)
|
|
601
661
|
# => Assignment, including signing_urls
|
|
602
662
|
```
|
|
603
663
|
|
|
604
|
-
|
|
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:
|
|
605
667
|
|
|
606
668
|
```ruby
|
|
607
669
|
role_signer = {
|
|
608
|
-
role_id: template.fetch('roles').
|
|
670
|
+
role_id: template.fetch('roles').find { |role| role['assignment_type'] == 'Signer' }.fetch('id'),
|
|
609
671
|
id: signer.fetch('id'),
|
|
610
672
|
verification_method: 'Email',
|
|
611
673
|
notification_methods: ['Email']
|
|
@@ -678,7 +740,8 @@ signer is created.
|
|
|
678
740
|
Requesting the method is fully supported. Completing the signature is not: the two-step handshake
|
|
679
741
|
(`/signers/certificate/start`, `/signers/certificate/complete`) is absent from the API v1 machine contract — its
|
|
680
742
|
authentication and request/response schemas are not published — so the SDK does not expose those completion
|
|
681
|
-
calls
|
|
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.
|
|
682
745
|
Once it completes, the `pades` artifact returns the qualified signature.
|
|
683
746
|
|
|
684
747
|
### 7. Inspect, tag, download, and verify
|
|
@@ -691,7 +754,8 @@ tags = client.documents.append_tags(document_id, ['tag-id']) # => Array<Tag>
|
|
|
691
754
|
client.documents.replace_tags(document_id, tags.map { |tag| tag.fetch('id') })
|
|
692
755
|
|
|
693
756
|
original_pdf = client.documents.download(document_id, 'original')
|
|
694
|
-
signed_pdf = client.documents.download(document_id, '
|
|
757
|
+
signed_pdf = client.documents.download(document_id, 'certificated')
|
|
758
|
+
# For documents completed with ICP-Brasil certificates: download(document_id, 'pades')
|
|
695
759
|
certificate_pdf = client.documents.download(document_id, 'certificated')
|
|
696
760
|
|
|
697
761
|
verification = client.documents.verify('signature-hash-from-assinafy')
|
|
@@ -744,10 +808,10 @@ result = client.upload_and_request_signatures(
|
|
|
744
808
|
source: { file_path: './contract.pdf' },
|
|
745
809
|
signers: [
|
|
746
810
|
{ full_name: 'Alice Silva', email: 'alice@example.com' },
|
|
747
|
-
{ full_name: 'Bob Costa',
|
|
811
|
+
{ full_name: 'Bob Costa', email: 'bob@example.com' }
|
|
748
812
|
],
|
|
749
813
|
message: 'Please sign.',
|
|
750
|
-
expires_at: '
|
|
814
|
+
expires_at: '2099-12-31T23:59:00Z'
|
|
751
815
|
)
|
|
752
816
|
|
|
753
817
|
puts result[:document]['id']
|
|
@@ -760,11 +824,11 @@ result[:signer_ids] # => ['<sid-1>', '<sid-2>']
|
|
|
760
824
|
The SDK raises one of:
|
|
761
825
|
|
|
762
826
|
- `Assinafy::ValidationError` — caller-side input invalid (missing IDs, bad email, etc.).
|
|
763
|
-
- `Assinafy::ApiError` — the API returned a non-2xx status. Includes `status_code` and `response_data
|
|
827
|
+
- `Assinafy::ApiError` — the API returned a non-2xx status. Includes `status_code` and `response_data`; on a
|
|
828
|
+
`403`, `context[:www_authenticate]` names the scope an OAuth token is missing.
|
|
764
829
|
- `Assinafy::OAuthError` — an OAuth endpoint failed. Subclasses `ApiError`, and adds `error` and
|
|
765
|
-
`error_description` from the flat RFC 6749 body
|
|
766
|
-
|
|
767
|
-
- `Assinafy::NetworkError` — Faraday connection error or timeout.
|
|
830
|
+
`error_description` from the flat RFC 6749 body.
|
|
831
|
+
- `Assinafy::NetworkError` — Faraday connection error, timeout, or TLS failure.
|
|
768
832
|
- `Assinafy::Error` — base class; other unexpected errors get wrapped here with the operation label.
|
|
769
833
|
|
|
770
834
|
All inherit a `#context` Hash with debugging metadata.
|
|
@@ -798,10 +862,16 @@ ASSINAFY_API_KEY=... \
|
|
|
798
862
|
ASSINAFY_ACCOUNT_ID=... \
|
|
799
863
|
ASSINAFY_TEST_EMAIL=recipient1@example.com \
|
|
800
864
|
ASSINAFY_TEST_EMAIL2=recipient2@example.com \
|
|
865
|
+
ASSINAFY_TEMPLATE_ID=configured-template-id \
|
|
801
866
|
ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
|
|
802
867
|
bundle exec rspec spec/integration
|
|
803
868
|
```
|
|
804
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
|
+
|
|
805
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`.
|
|
806
876
|
|
|
807
877
|
## Contributing
|