@assinafy/sdk 2.0.0 → 2.1.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.
package/README.md CHANGED
@@ -2,16 +2,22 @@
2
2
 
3
3
  TypeScript SDK for the [Assinafy API](https://api.assinafy.com.br/v1/docs) — a Brazilian digital signature platform.
4
4
 
5
- Covers the server-side surface of the API: documents, signers, assignments, templates, tags, workspaces, webhooks, field definitions, authentication, public/signer-side flows, and the high-level `uploadAndRequestSignatures` helper.
5
+ Covers all 89 operations in the current official OpenAPI document: accounts,
6
+ authentication, users, documents, assignments, signers, signer-side flows,
7
+ templates, tags, fields, webhooks, branding, statistics, and the high-level
8
+ `uploadAndRequestSignatures` workflow. Five additional template-management
9
+ routes exposed by the live API are retained as compatibility extensions.
6
10
 
7
- Deliberately not wrapped: the browser-redirect OAuth endpoints (`/auth/authenticate`, `/auth/link-social-login`, `/login-callback`), which a server-side SDK cannot meaningfully drive, and the account `theme`/`logo` branding routes.
11
+ See [API coverage](docs/API_COVERAGE.md) for the exhaustive operation map and
12
+ [compatibility notes](docs/COMPATIBILITY.md) for the few places where live
13
+ behavior differs from the published schema.
8
14
 
9
15
  ## Requirements
10
16
 
11
- - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Tested
12
- on 22 (maintenance LTS) and 24 (active LTS); Node 20 reached end-of-life in
13
- April 2026 and is no longer supported.
14
- - or Bun 1.0+
17
+ - Node.js 22+ for the built-in `FormData` / `Blob` APIs used by uploads. Packed
18
+ CJS and ESM imports are tested on 22 (maintenance LTS), 24 (active LTS), and
19
+ 26 (Current); Node 20 reached end-of-life in April 2026 and is unsupported.
20
+ - or Bun 1.3.14 (the version pinned for development and CI)
15
21
 
16
22
  ## Installation
17
23
 
@@ -36,7 +42,6 @@ import { AssinafyClient } from '@assinafy/sdk';
36
42
  const client = new AssinafyClient({
37
43
  apiKey: process.env.ASSINAFY_API_KEY!,
38
44
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
39
- webhookSecret: process.env.ASSINAFY_WEBHOOK_SECRET,
40
45
  });
41
46
 
42
47
  const result = await client.uploadAndRequestSignatures({
@@ -59,35 +64,53 @@ The API supports two authentication methods. Prefer `apiKey` — it maps to the
59
64
  // Preferred: X-Api-Key header
60
65
  new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
61
66
 
62
- // Legacy: Authorization: Bearer <token>
67
+ // Access token: Authorization: Bearer <token>
63
68
  new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
64
69
  ```
65
70
 
71
+ Credentials are optional at construction time. A credentialless client uses a
72
+ separate, auth-free transport for public authentication and signer-access-code
73
+ operations, so an API key or Bearer token is never attached accidentally:
74
+
75
+ ```ts
76
+ const publicClient = new AssinafyClient({
77
+ baseUrl: 'https://sandbox.assinafy.com.br/v1',
78
+ });
79
+
80
+ await publicClient.auth.login('me@example.com', 'password');
81
+ await publicClient.documents.getPublic(documentId);
82
+ await publicClient.signerDocuments.self(signerAccessCode);
83
+ ```
84
+
85
+ Protected methods still require `apiKey` or `token`; the API returns its normal
86
+ `401` response if one is called without credentials.
87
+
66
88
  ## Configuration
67
89
 
68
90
  | Option | Type | Default | Description |
69
91
  | --------------- | -------- | --------------------------------------- | --------------------------------------------- |
70
92
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
71
- | `token` | string | — | Legacy access token (sent as `Bearer`). |
93
+ | `token` | string | — | Access token (sent as `Authorization: Bearer`). |
72
94
  | `accountId` | string | — | Default workspace/account ID. |
73
95
  | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL (e.g. the sandbox). |
74
- | `webhookSecret` | string | — | Shared secret used by `WebhookVerifier`. |
96
+ | `webhookSecret` | string | — | Opt-in HMAC secret used by `WebhookVerifier`; see its [contract caveat](docs/COMPATIBILITY.md#webhook-signature-verification-is-not-in-the-openapi-contract). |
75
97
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
76
- | `maxRetries` | number | `2` | Auto-retries on HTTP 429, honoring `Retry-After`. `0` disables. |
98
+ | `maxRetries` | number | `2` | Auto-retries eligible HTTP 429 responses, honoring `Retry-After`. `0` disables. |
77
99
  | `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
78
100
 
79
101
  ### Rate limiting
80
102
 
81
- The API allows ~120 requests/minute and returns `X-Rate-Limit-*` headers. On an
82
- HTTP `429`, the client automatically retries up to `maxRetries` times, waiting
83
- for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay before
84
- each attempt. Only `429` is retried, so non-idempotent calls are safe.
103
+ On an HTTP `429`, the client automatically retries up to `maxRetries` times,
104
+ waiting for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay
105
+ before each attempt. Automatic replay is limited to `GET`, `HEAD`, `OPTIONS`,
106
+ `PUT`, and `DELETE`. A `POST` or `PATCH` is retried only when the request has a
107
+ non-empty `Idempotency-Key` header. No other HTTP status is retried.
85
108
 
86
109
  ### Factories
87
110
 
88
111
  ```ts
89
112
  // Positional factory
90
- const client = AssinafyClient.create('api-key', 'account-id', { webhookSecret: 'shhh' });
113
+ const client = AssinafyClient.create('api-key', 'account-id');
91
114
 
92
115
  // From a plain object (accepts snake_case or camelCase keys)
93
116
  const client = AssinafyClient.fromConfig({
@@ -98,7 +121,9 @@ const client = AssinafyClient.fromConfig({
98
121
 
99
122
  ## Endpoint coverage
100
123
 
101
- Every public endpoint documented in https://api.assinafy.com.br/v1/docs is covered. The table below maps each resource to its API surface.
124
+ All 89 operations documented at https://api.assinafy.com.br/v1/docs are
125
+ covered. The table below is the resource-level summary; the auditable,
126
+ operation-by-operation ledger is in [docs/API_COVERAGE.md](docs/API_COVERAGE.md).
102
127
 
103
128
  | Resource | Endpoints |
104
129
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -107,16 +132,27 @@ Every public endpoint documented in https://api.assinafy.com.br/v1/docs is cover
107
132
  | `client.assignments` | **list**, create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
108
133
  | `client.templates` | **create**, list, get, **update**, **delete**, downloadPage |
109
134
  | `client.tags` | list, create, update, delete |
110
- | `client.workspaces` | create, list, get, update, delete |
135
+ | `client.workspaces` | create, list, get, update, delete, getTheme, downloadLogo, uploadLogo, deleteLogo, getStats |
111
136
  | `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
112
137
  | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
113
- | `client.auth` | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
138
+ | `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
139
+ | `client.users` | getCurrent, getStats |
114
140
  | `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
115
141
  | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
116
142
 
143
+ Every HTTP wrapper has TypeScript-checked request/response shapes and
144
+ method-level JSDoc covering the wire payload, return shape, validation,
145
+ relevant API errors, and a copyable example. Reusable and OpenAPI schema-level
146
+ payloads are exported as named types; small method-local option bags remain
147
+ inline in the generated declarations. Editors expose the reference on hover,
148
+ and declaration files ship with the package. The coverage ledger links those
149
+ typed methods back to each upstream operation without duplicating the schema.
150
+
117
151
  ## Resources
118
152
 
119
- Most account-scoped methods accept an optional `accountId` that overrides the client default. Workspace `get/update/delete` always require an explicit account ID.
153
+ Most account-scoped methods accept an optional `accountId` that overrides the
154
+ client default. Workspace `get`, `update`, `delete`, branding, and statistics
155
+ methods always require an explicit account ID.
120
156
 
121
157
  ### Documents
122
158
 
@@ -167,21 +203,30 @@ await client.documents.isFullySigned(doc.id);
167
203
  await client.documents.getSigningProgress(doc.id);
168
204
  await client.documents.delete(doc.id);
169
205
 
170
- // Verify a signed document by its SHA-1 hash
206
+ // Verify a signed document by its Assinafy signature hash
171
207
  await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
172
208
 
173
209
  // Public endpoints (no auth)
174
210
  await client.documents.getPublic(doc.id);
175
- await client.documents.sendToken(doc.id, 'jane@example.com', 'email');
211
+ // Official request body: { email: 'jane@example.com' }
212
+ await client.documents.sendToken(doc.id, 'jane@example.com');
213
+
214
+ // Explicit compatibility overload for older deployments:
215
+ // { recipient: '+5548999990000', channel: 'whatsapp' }
216
+ await client.documents.sendToken(doc.id, '+5548999990000', 'whatsapp');
176
217
 
177
- // Tags attached to a document (by tag name; unknown names are auto-created)
218
+ // The current OpenAPI contract requires existing tag IDs.
219
+ const contractsTag = await client.tags.create({ name: 'Contracts' });
220
+ const quarterTag = await client.tags.create({ name: '2026-Q1' });
221
+ const urgentTag = await client.tags.create({ name: 'Urgent' });
178
222
  await client.documents.listTags(doc.id);
179
- await client.documents.replaceTags(doc.id, ['Contracts', '2026-Q1']); // [] detaches all
180
- await client.documents.addTags(doc.id, ['Urgent']); // append, idempotent
181
- await client.documents.detachTag(doc.id, tagId); // remove one
223
+ await client.documents.replaceTags(doc.id, [contractsTag.id, quarterTag.id]); // [] detaches all
224
+ await client.documents.addTags(doc.id, [urgentTag.id]); // append
225
+ await client.documents.detachTag(doc.id, urgentTag.id); // remove one
182
226
  ```
183
227
 
184
- Uploads are validated locally: only `.pdf` files up to 25 MB are accepted (the API's current hard limit).
228
+ Uploads are validated locally: only `.pdf` files up to 25 MB whose bytes begin
229
+ with the PDF magic header (`%PDF-`) are accepted (the API's current hard limit).
185
230
 
186
231
  List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
187
232
 
@@ -198,12 +243,15 @@ await client.signers.create({
198
243
  // whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
199
244
  // (note: `cpf` is accepted on input but never echoed back by the API)
200
245
 
201
- // `email` is optional a WhatsApp-only signer is valid (at least one is required)
246
+ // Both contacts are optional in the create endpoint. A WhatsApp-only signer:
202
247
  await client.signers.create({
203
248
  full_name: 'WhatsApp Only',
204
249
  whatsapp_phone_number: '+5548999990000',
205
250
  });
206
251
 
252
+ // Name-only is valid too, but cannot be notified until a contact is added.
253
+ await client.signers.create({ full_name: 'Contact Pending' });
254
+
207
255
  // PHP SDK compatibility aliases are also accepted
208
256
  await client.signers.create({
209
257
  full_name: 'Jane Doe',
@@ -246,8 +294,8 @@ await client.assignments.create(documentId, {
246
294
  ],
247
295
  });
248
296
 
249
- // Estimate cost (signers may omit `id` when only the channel matters) → ICostEstimate
250
- await client.assignments.estimateCost(documentId, { signers: ['signer-1'] });
297
+ // Estimate cost (the endpoint prices channel descriptors, not signer IDs) → ICostEstimate
298
+ await client.assignments.estimateCost(documentId, { signers: [{}] }); // default Email
251
299
  await client.assignments.estimateCost(documentId, {
252
300
  signers: [{ verification_method: 'Whatsapp' }],
253
301
  });
@@ -283,9 +331,17 @@ await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No l
283
331
 
284
332
  ### Templates
285
333
 
334
+ `templates.list()` is part of the current OpenAPI document. The live API also
335
+ exposes five template-management routes—`create`, `get`, `update`, `delete`,
336
+ and `downloadPage`—that are retained as tested compatibility extensions even
337
+ though they are absent from that document. See
338
+ [compatibility notes](docs/COMPATIBILITY.md#template-management-live-extensions).
339
+ Template status casing has varied between published examples and live
340
+ responses; compare `template.status.toLowerCase()` when branching on it.
341
+
286
342
  ```ts
287
343
  // Create a template by uploading a PDF (multipart). The template starts in
288
- // `Uploaded` status and becomes `Ready` once its pages are processed.
344
+ // an uploaded state and becomes ready once its pages are processed.
289
345
  const created = await client.templates.create(
290
346
  { filePath: './nda.pdf' }, // or { buffer, fileName: 'nda.pdf' }
291
347
  { name: 'NDA template' },
@@ -301,18 +357,28 @@ const created = await client.templates.create(
301
357
  const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
302
358
  const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
303
359
  await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
304
- await client.templates.downloadPage(created.id, template.pages![0].id); // → Buffer (JPEG)
360
+ const firstPage = template.pages?.[0];
361
+ if (firstPage) await client.templates.downloadPage(created.id, firstPage.id); // → Buffer (JPEG)
305
362
  await client.templates.delete(created.id);
306
363
 
307
- // Create a *document* from a template (each signer maps to a template role)
364
+ // Create a document from an existing, configured template. Fresh uploads have
365
+ // only an Editor role; add signer roles in Assinafy's editor first.
366
+ const configured = await client.templates.get(templateId);
367
+ const signerRole = configured.roles?.find(
368
+ (role) => typeof role.assignment_type === 'string'
369
+ && role.assignment_type.toLowerCase() !== 'editor',
370
+ );
371
+ if (!signerRole) throw new Error('Template has no signer role');
308
372
  await client.documents.createFromTemplate(
309
373
  templateId,
310
- [{ role_id: template.roles![0].id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
374
+ [{ role_id: signerRole.id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
311
375
  { name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
312
376
  );
313
377
 
314
378
  // Estimate the cost before creating → ICostEstimate
315
- await client.documents.estimateCostFromTemplate(templateId, [{ role_id: 'role_id', id: signerId }]);
379
+ await client.documents.estimateCostFromTemplate(templateId, [
380
+ { role_id: 'role_id', verification_method: 'Email', notification_methods: ['Email'] },
381
+ ]);
316
382
  // → { documents: 1, total_credits: 0, document_balance: 67, credit_balance: 0,
317
383
  // has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
318
384
  ```
@@ -337,12 +403,49 @@ Attach/detach tags on a specific document via `client.documents.listTags / repla
337
403
 
338
404
  ### Workspaces
339
405
 
406
+ The official create/update request schemas define `name` and
407
+ `notification_sender_type`. The sandbox also accepts the color fields shown
408
+ below; they are retained as a documented compatibility extension.
409
+
340
410
  ```ts
341
- await client.workspaces.create({ name: 'My Workspace', primary_color: '#ff0066' });
411
+ // Colours are 6-char hex WITHOUT a leading '#' (unlike tags, which strip it).
412
+ // '#ff0066' is rejected — the account endpoints want exactly 6 characters.
413
+ await client.workspaces.create({
414
+ name: 'My Workspace',
415
+ notification_sender_type: 'Account',
416
+ primary_color: 'ff0066',
417
+ secondary_color: '0066ff',
418
+ });
419
+ // → { id, name, primary_color: 'ff0066', secondary_color: '0066ff', created_at }
342
420
  await client.workspaces.list();
343
421
  await client.workspaces.get(accountId);
344
- await client.workspaces.update(accountId, { name: 'Renamed' });
422
+ await client.workspaces.update(accountId, {
423
+ name: 'Renamed',
424
+ notification_sender_type: 'User',
425
+ primary_color: '112233',
426
+ });
427
+
428
+ // Branding
429
+ const theme = await client.workspaces.getTheme(accountId);
430
+ const logo = await client.workspaces.downloadLogo(accountId); // Buffer
431
+ await client.workspaces.uploadLogo(accountId, { filePath: './logo.png' });
432
+ await client.workspaces.uploadLogo(accountId, {
433
+ buffer: logoBuffer,
434
+ fileName: 'logo.png',
435
+ contentType: 'image/png',
436
+ });
437
+ await client.workspaces.deleteLogo(accountId);
438
+
439
+ // Latest 12 months by default; daily statistics require a YYYY-MM month.
440
+ await client.workspaces.getStats(accountId);
441
+ await client.workspaces.getStats(accountId, {
442
+ granularity: 'daily',
443
+ month: '2026-06',
444
+ });
445
+
345
446
  await client.workspaces.delete(accountId);
447
+ // If the API reports deletion restrictions, explicitly opt in to overriding them:
448
+ await client.workspaces.delete(restrictedAccountId, { force: true });
346
449
  ```
347
450
 
348
451
  ### Field definitions
@@ -377,8 +480,14 @@ await client.fields.listTypes();
377
480
  Most server-side integrations should just use `X-Api-Key` directly. Use these endpoints when you need to bootstrap a session for a human user.
378
481
 
379
482
  ```ts
483
+ // Browser OAuth: redirect the user to this URL. The callback helper returns the
484
+ // Assinafy callback URL for provider configuration; neither follows a redirect.
485
+ const oauthStart = client.auth.getSocialLoginUrl('google');
486
+ const oauthCallback = client.auth.getSocialLoginCallbackUrl();
487
+
380
488
  const { access_token, user, accounts } = await client.auth.login('me@example.com', 'pw');
381
489
  await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });
490
+ await client.auth.linkSocialLogin({ provider: 'google', token: 'google-id-token' });
382
491
 
383
492
  // Personal API key
384
493
  await client.auth.createApiKey('current-password');
@@ -391,6 +500,23 @@ await client.auth.requestPasswordReset('me@example.com');
391
500
  await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });
392
501
  ```
393
502
 
503
+ ### Authenticated user
504
+
505
+ ```ts
506
+ const user = await client.users.getCurrent();
507
+ // → { id, name, email, telephone, government_id, is_email_verified,
508
+ // has_accepted_terms, created_at, to_be_deleted_at }
509
+
510
+ // Cross-account document funnel, latest 12 monthly periods by default.
511
+ const monthly = await client.users.getStats();
512
+ const daily = await client.users.getStats({
513
+ granularity: 'daily',
514
+ month: '2026-06',
515
+ });
516
+ // Each row includes period, documents_uploaded, documents_sent,
517
+ // signature_requests by channel/view/completion, and documents_certified.
518
+ ```
519
+
394
520
  ### Webhooks
395
521
 
396
522
  ```ts
@@ -416,22 +542,35 @@ await client.webhooks.retryDispatch(dispatchId);
416
542
 
417
543
  ### Webhook verification
418
544
 
419
- Webhook payloads are signed with HMAC-SHA256 of the raw body using the workspace `webhookSecret`. Assinafy sends the hex digest in the `X-Assinafy-Signature` header.
545
+ `WebhookVerifier` is an opt-in HMAC-SHA256 utility for integrations whose
546
+ Assinafy environment provides a shared secret and signature header. The
547
+ current official OpenAPI document does **not** define a webhook signature
548
+ scheme or header name. Confirm the delivery contract for your environment
549
+ before enabling this check; do not reject production callbacks based on an
550
+ assumed header. The example below uses an application-configured header name.
420
551
 
421
552
  ```ts
422
553
  import express from 'express';
423
554
 
555
+ const webhookSecret = process.env.ASSINAFY_WEBHOOK_SECRET;
556
+ const signatureHeader = process.env.ASSINAFY_SIGNATURE_HEADER;
557
+ if (!webhookSecret || !signatureHeader) {
558
+ throw new Error('This deployment has no confirmed webhook-signature contract');
559
+ }
560
+
561
+ const webhookClient = new AssinafyClient({ webhookSecret });
562
+
424
563
  app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
425
- const signature = req.header('x-assinafy-signature') ?? '';
564
+ const signature = req.header(signatureHeader) ?? '';
426
565
  const rawBody = req.body as Buffer;
427
566
 
428
- if (!client.webhookVerifier.verify(rawBody, signature)) {
567
+ if (!webhookClient.webhookVerifier.verify(rawBody, signature)) {
429
568
  return res.status(401).send('Invalid signature');
430
569
  }
431
570
 
432
- const event = client.webhookVerifier.extractEvent(rawBody);
433
- const type = client.webhookVerifier.getEventType(event);
434
- const data = client.webhookVerifier.getEventData(event);
571
+ const event = webhookClient.webhookVerifier.extractEvent(rawBody);
572
+ const type = webhookClient.webhookVerifier.getEventType(event);
573
+ const data = webhookClient.webhookVerifier.getEventData(event);
435
574
 
436
575
  switch (type) {
437
576
  case 'document_ready': break;
@@ -445,7 +584,10 @@ app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req,
445
584
 
446
585
  ### Signer-side endpoints
447
586
 
448
- For building custom signer portals. Every call requires the `signer-access-code` URL parameter that Assinafy emails/whatsapps to the signer.
587
+ For building custom signer portals. Most calls require the `signer-access-code`
588
+ URL parameter that Assinafy emails/whatsapps to the signer. Artifact download is
589
+ the documented public exception; its optional fourth access-code argument exists
590
+ only for compatibility with deployments that still expect the legacy query.
449
591
 
450
592
  ```ts
451
593
  await client.signerDocuments.self(accessCode);
@@ -453,19 +595,20 @@ await client.signerDocuments.acceptTerms(accessCode);
453
595
  await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
454
596
 
455
597
  await client.signerDocuments.getCurrent(signerId, accessCode);
456
- const { data } = await client.signerDocuments.list(signerId, accessCode, { search: 'invoice' });
598
+ const { data } = await client.signerDocuments.list(signerId, accessCode, { per_page: 20 });
457
599
  // Signer-side counterpart of documents.search(), authorised by the access code.
458
600
  const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
459
- await client.signerDocuments.download(signerId, documentId, 'original', accessCode);
601
+ await client.signerDocuments.download(signerId, documentId, 'original');
460
602
 
461
603
  await client.signerDocuments.confirmData(documentId, accessCode, {
462
604
  email: 'me@example.com',
463
- whatsapp_phone_number: '+5548999990000',
464
- has_accepted_terms: true,
605
+ full_name: 'Example Signer',
606
+ government_id: '123.456.789-00',
465
607
  });
608
+ await client.signerDocuments.acceptTerms(accessCode);
466
609
 
467
- // Signature image management
468
- await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature' });
610
+ // Signature image management ({ reuse: true } persists it for future documents)
611
+ await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature', reuse: true });
469
612
  await client.signerDocuments.downloadSignature(accessCode, 'signature');
470
613
 
471
614
  // Sign / decline
@@ -497,14 +640,21 @@ const result = await client.uploadAndRequestSignatures({
497
640
  expiresAt: '2026-12-31T00:00:00Z',
498
641
  });
499
642
 
500
- result.document; // IDocumentUploadResponse
643
+ result.document; // fully-processed IDocumentDetailsResponse (waitForReady: true, the default);
644
+ // the raw IDocumentUploadResponse when waitForReady: false
501
645
  result.assignment; // IAssignment
502
646
  result.signer_ids; // string[]
503
647
  ```
504
648
 
649
+ `waitForReady: false` skips only the final post-assignment document re-fetch.
650
+ The helper always waits for the uploaded document to reach a metadata-ready
651
+ state before creating its assignment; that safety wait is required to avoid a
652
+ processing race.
653
+
505
654
  ## Errors
506
655
 
507
- Every method rejects with an `AssinafyError` subclass.
656
+ HTTP methods reject with an `AssinafyError` subclass. Synchronous helpers such
657
+ as `getSocialLoginUrl()` can throw `ValidationError` before any request.
508
658
 
509
659
  ```ts
510
660
  import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
@@ -526,25 +676,52 @@ try {
526
676
 
527
677
  ## Live smoke test
528
678
 
529
- A real-network test script under [`scripts/live-smoke.ts`](scripts/live-smoke.ts) exercises the full API. Use it to sanity-check a workspace before shipping.
679
+ A redaction-safe real-network audit under
680
+ [`scripts/live-smoke.ts`](scripts/live-smoke.ts) exercises read-only operations
681
+ by default. Its `--all` mode creates an isolated sandbox workspace, audits
682
+ reversible CRUD, upload, assignment, branding, statistics, and template flows,
683
+ then attempts per-resource cleanup and force-deletes that workspace in `finally`.
684
+ It complements the unit/contract suites. Optional login credentials enable the
685
+ password-login probe; an optional signer access code/OTP enables read-only
686
+ signer-link and OTP probes; and an optional HTTPS webhook receiver enables the
687
+ subscription lifecycle. Password changes, API-key rotation, social-provider
688
+ login/linking, legal consent, identity/signature mutation, signing, and decline
689
+ flows are deliberately never automated by this harness and are always reported
690
+ as explicit `SKIP`s. They require credentials, authorization, or legal fixtures
691
+ that an API key/account/e-mail set cannot safely supply.
530
692
 
531
693
  ```bash
532
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts # read-only
533
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write # also creates+deletes a signer
534
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload # also uploads a PDF + a template, then deletes both
694
+ # Read-only account/API checks. The base URL is deliberately mandatory.
695
+ ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
696
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… \
697
+ bun scripts/live-smoke.ts
698
+
699
+ # Full reversible audit in a disposable sandbox workspace.
700
+ ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1 \
701
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… \
702
+ ASSINAFY_TEST_EMAIL_PRIMARY=first@example.com \
703
+ ASSINAFY_TEST_EMAIL_SECONDARY=second@example.com \
704
+ bun scripts/live-smoke.ts --all
535
705
  ```
536
706
 
537
- Set `ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1` to run it against the
538
- sandbox instead of production.
707
+ Mutation is refused unless the URL's exact host is the Assinafy sandbox. The
708
+ explicit `--confirm-production` escape hatch exists for controlled environments
709
+ but should not be used for routine SDK verification. The script never prints
710
+ credentials, IDs, e-mail addresses, URLs, request/response payloads, or raw API
711
+ errors. See [CONTRIBUTING.md](CONTRIBUTING.md#sandbox-tests) for optional fixture
712
+ variables and safety guidance.
539
713
 
540
714
  ## Development
541
715
 
542
716
  ```bash
543
- bun install # or npm install
544
- bun test # runs bun:test suites (Bun is required for tests)
545
- npm run typecheck # tsc --noEmit
546
- npm run lint
547
- npm run build # tsup → dist/ (CJS + ESM + .d.ts)
717
+ bun install # or npm install
718
+ bun run typecheck # source, script, and test type checks
719
+ bun run lint
720
+ bun test # bun:test suites
721
+ bun run test:coverage
722
+ bun run build # tsup → dist/ (CJS + ESM + .d.ts)
723
+ bun run lint:pkg # publint + arethetypeswrong
724
+ bun run verify # complete local release gate
548
725
  ```
549
726
 
550
727
  ## License
package/SECURITY.md ADDED
@@ -0,0 +1,59 @@
1
+ # Security policy
2
+
3
+ ## Supported releases
4
+
5
+ Security fixes are developed on the canonical default branch and released on
6
+ the current supported major. Unless a separate support agreement states
7
+ otherwise, users should run the latest published patch. Older majors and
8
+ superseded minor releases may not receive fixes.
9
+
10
+ ## Reporting a vulnerability
11
+
12
+ Do not disclose a suspected vulnerability in a public issue, discussion, merge
13
+ request, commit message, or CI log.
14
+
15
+ Use the canonical GitLab project's confidential vulnerability-reporting
16
+ channel, or the GitHub mirror's private **Security → Report a vulnerability**
17
+ flow when it is enabled. Verify that the report is private before adding
18
+ sensitive details. If neither private channel is available, open a public issue
19
+ that asks maintainers for a private contact method and include no vulnerability
20
+ details.
21
+
22
+ Provide:
23
+
24
+ - the affected SDK and runtime versions;
25
+ - the affected method, endpoint, and authentication mode;
26
+ - impact and realistic attack preconditions;
27
+ - a minimal, sanitized reproducer or failing test;
28
+ - any proposed mitigation or patch; and
29
+ - whether the issue has been disclosed elsewhere.
30
+
31
+ Do not include usable API keys, bearer tokens, signer access codes, account
32
+ identifiers, webhook secrets, documents, or personal data. Replace them with
33
+ clearly fake values. If a real credential was exposed, revoke or rotate it at
34
+ once; deleting the message or git commit is not sufficient.
35
+
36
+ Maintainers will confirm receipt through the private channel, investigate the
37
+ supported release, coordinate a fix and advisory, and publish a patched version
38
+ when appropriate. Public disclosure should wait until users have a reasonable
39
+ opportunity to upgrade.
40
+
41
+ ## Security expectations
42
+
43
+ - Protected resources require explicit account credentials. Public,
44
+ authentication, and signer-access-code flows use an auth-free transport so
45
+ configured account credentials are not sent to public endpoints.
46
+ - Consumers must use HTTPS endpoints and protect all SDK configuration as
47
+ secrets. Debug logging and error telemetry must be reviewed for response data
48
+ before being enabled in production.
49
+ - The webhook HMAC helper is an opt-in utility, not proof of an official
50
+ Assinafy signing contract. Confirm the header, algorithm, encoding, and secret
51
+ delivery mechanism with Assinafy before enforcing it. See
52
+ `docs/COMPATIBILITY.md`.
53
+ - Dependency changes must keep `bun.lock` synchronized and pass `bun run audit`.
54
+ GitHub Actions are pinned to immutable commit SHAs and updated through
55
+ Dependabot review.
56
+ - Sandbox credentials and fixtures must never be reused in production. Live
57
+ mutation tests belong only in dedicated, disposable sandbox accounts.
58
+
59
+ For general defects without a security impact, use the normal issue tracker.