@assinafy/sdk 1.5.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,12 +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
- Provides 100% endpoint coverage of the public 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.
10
+
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.
6
14
 
7
15
  ## Requirements
8
16
 
9
- - Node.js 20+ (current LTS) for the built-in `FormData` / `Blob` APIs used by uploads
10
- - 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)
11
21
 
12
22
  ## Installation
13
23
 
@@ -32,7 +42,6 @@ import { AssinafyClient } from '@assinafy/sdk';
32
42
  const client = new AssinafyClient({
33
43
  apiKey: process.env.ASSINAFY_API_KEY!,
34
44
  accountId: process.env.ASSINAFY_ACCOUNT_ID!,
35
- webhookSecret: process.env.ASSINAFY_WEBHOOK_SECRET,
36
45
  });
37
46
 
38
47
  const result = await client.uploadAndRequestSignatures({
@@ -55,35 +64,53 @@ The API supports two authentication methods. Prefer `apiKey` — it maps to the
55
64
  // Preferred: X-Api-Key header
56
65
  new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
57
66
 
58
- // Legacy: Authorization: Bearer <token>
67
+ // Access token: Authorization: Bearer <token>
59
68
  new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
60
69
  ```
61
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
+
62
88
  ## Configuration
63
89
 
64
90
  | Option | Type | Default | Description |
65
91
  | --------------- | -------- | --------------------------------------- | --------------------------------------------- |
66
92
  | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
67
- | `token` | string | — | Legacy access token (sent as `Bearer`). |
93
+ | `token` | string | — | Access token (sent as `Authorization: Bearer`). |
68
94
  | `accountId` | string | — | Default workspace/account ID. |
69
95
  | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL (e.g. the sandbox). |
70
- | `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). |
71
97
  | `timeout` | number | `30000` | Request timeout in milliseconds. |
72
- | `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. |
73
99
  | `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
74
100
 
75
101
  ### Rate limiting
76
102
 
77
- The API allows ~120 requests/minute and returns `X-Rate-Limit-*` headers. On an
78
- HTTP `429`, the client automatically retries up to `maxRetries` times, waiting
79
- for the server-provided `Retry-After` (or `X-Rate-Limit-Reset`) delay before
80
- 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.
81
108
 
82
109
  ### Factories
83
110
 
84
111
  ```ts
85
112
  // Positional factory
86
- const client = AssinafyClient.create('api-key', 'account-id', { webhookSecret: 'shhh' });
113
+ const client = AssinafyClient.create('api-key', 'account-id');
87
114
 
88
115
  // From a plain object (accepts snake_case or camelCase keys)
89
116
  const client = AssinafyClient.fromConfig({
@@ -94,25 +121,38 @@ const client = AssinafyClient.fromConfig({
94
121
 
95
122
  ## Endpoint coverage
96
123
 
97
- 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).
98
127
 
99
128
  | Resource | Endpoints |
100
129
  | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | `client.documents` | list, upload, details, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, **getPublic**, **sendToken**, **listTags**, **replaceTags**, **addTags**, **detachTag**, isFullySigned, getSigningProgress |
130
+ | `client.documents` | list, **search**, upload, details, get, **rename**, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, getPublic, sendToken, listTags, replaceTags, addTags, detachTag, isFullySigned, getSigningProgress |
102
131
  | `client.signers` | create, get, list, update, delete, findByEmail |
103
- | `client.assignments` | create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
132
+ | `client.assignments` | **list**, create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications |
104
133
  | `client.templates` | **create**, list, get, **update**, **delete**, downloadPage |
105
134
  | `client.tags` | list, create, update, delete |
106
- | `client.workspaces` | create, list, get, update, delete |
107
- | `client.webhooks` | register, get, inactivate, delete, listEventTypes, listDispatches, retryDispatch |
135
+ | `client.workspaces` | create, list, get, update, delete, getTheme, downloadLogo, uploadLogo, deleteLogo, getStats |
136
+ | `client.webhooks` | register, get, inactivate, listEventTypes, listDispatches, retryDispatch |
108
137
  | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
109
- | `client.auth` | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
110
- | `client.signerDocuments` | getCurrent, list, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
138
+ | `client.auth` | getSocialLoginUrl, getSocialLoginCallbackUrl, login, socialLogin, linkSocialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
139
+ | `client.users` | getCurrent, getStats |
140
+ | `client.signerDocuments` | getCurrent, list, **search**, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
111
141
  | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
112
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
+
113
151
  ## Resources
114
152
 
115
- 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.
116
156
 
117
157
  ### Documents
118
158
 
@@ -120,8 +160,12 @@ Most account-scoped methods accept an optional `accountId` that overrides the cl
120
160
  // Upload from a file path (recommended)
121
161
  const doc = await client.documents.upload(
122
162
  { filePath: './contract.pdf' },
123
- { metadata: { type: 'service' } },
163
+ { name: 'Service agreement', metadata: { type: 'service' } },
124
164
  );
165
+ // `name` is optional and defaults to the file's own name. The API derives the
166
+ // display name from the uploaded filename and appends `.pdf` when absent, so
167
+ // the document above is stored as 'Service agreement.pdf'. Accents are
168
+ // transliterated by the API ('Contrato de Serviço' → 'Contrato de Servico.pdf').
125
169
  // → {
126
170
  // resource: 'document', id: '1031…', account_id: '102d…', template_id: null,
127
171
  // name: 'contract.pdf', status: 'uploaded',
@@ -136,10 +180,20 @@ await client.documents.upload({ buffer, fileName: 'contract.pdf' });
136
180
 
137
181
  // List → { data: IDocumentListItem[], meta?: { current_page, per_page, total, last_page } }
138
182
  const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '-created_at' });
183
+
184
+ // Search is the lightweight alternative to list: same item shape, but the API
185
+ // skips the expanded `assignment`/`pages`. Prefer it for name lookups.
186
+ const hits = await client.documents.search({ search: 'agreement', status: 'pending_signature', 'per-page': 20 });
187
+
139
188
  await client.documents.details(doc.id);
140
189
  await client.documents.activities(doc.id);
141
190
  await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
142
191
 
192
+ // Rename. The API rejects this with 400 while the document is still in
193
+ // `metadata_processing`, so await waitUntilReady() first on a fresh upload.
194
+ // (Passing `name` to upload() avoids both the round-trip and the race.)
195
+ await client.documents.rename(doc.id, 'Signed service agreement.pdf');
196
+
143
197
  await client.documents.download(doc.id, 'certificated'); // 'original' | 'certificated' | 'certificate-page' | 'bundle'
144
198
  await client.documents.thumbnail(doc.id);
145
199
  await client.documents.downloadPage(doc.id, pageId);
@@ -149,21 +203,30 @@ await client.documents.isFullySigned(doc.id);
149
203
  await client.documents.getSigningProgress(doc.id);
150
204
  await client.documents.delete(doc.id);
151
205
 
152
- // Verify a signed document by its SHA-1 hash
206
+ // Verify a signed document by its Assinafy signature hash
153
207
  await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
154
208
 
155
209
  // Public endpoints (no auth)
156
210
  await client.documents.getPublic(doc.id);
157
- 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');
158
217
 
159
- // 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' });
160
222
  await client.documents.listTags(doc.id);
161
- await client.documents.replaceTags(doc.id, ['Contracts', '2026-Q1']); // [] detaches all
162
- await client.documents.addTags(doc.id, ['Urgent']); // append, idempotent
163
- 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
164
226
  ```
165
227
 
166
- 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).
167
230
 
168
231
  List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
169
232
 
@@ -180,12 +243,15 @@ await client.signers.create({
180
243
  // whatsapp_phone_number: '+5548999990000', has_accepted_terms: false }
181
244
  // (note: `cpf` is accepted on input but never echoed back by the API)
182
245
 
183
- // `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:
184
247
  await client.signers.create({
185
248
  full_name: 'WhatsApp Only',
186
249
  whatsapp_phone_number: '+5548999990000',
187
250
  });
188
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
+
189
255
  // PHP SDK compatibility aliases are also accepted
190
256
  await client.signers.create({
191
257
  full_name: 'Jane Doe',
@@ -206,6 +272,10 @@ When an `email` is supplied, `signers.create()` is idempotent by email, matching
206
272
  ### Assignments
207
273
 
208
274
  ```ts
275
+ // List every assignment in the workspace.
276
+ // → { data: IAssignment[], meta?: { current_page, per_page, total, last_page } }
277
+ const { data, meta } = await client.assignments.list({ page: 1, 'per-page': 20 });
278
+
209
279
  // Signers may be ids or objects — the SDK normalises to the API shape.
210
280
  await client.assignments.create(documentId, {
211
281
  method: 'virtual',
@@ -224,8 +294,8 @@ await client.assignments.create(documentId, {
224
294
  ],
225
295
  });
226
296
 
227
- // Estimate cost (signers may omit `id` when only the channel matters) → ICostEstimate
228
- 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
229
299
  await client.assignments.estimateCost(documentId, {
230
300
  signers: [{ verification_method: 'Whatsapp' }],
231
301
  });
@@ -261,9 +331,17 @@ await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'No l
261
331
 
262
332
  ### Templates
263
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
+
264
342
  ```ts
265
343
  // Create a template by uploading a PDF (multipart). The template starts in
266
- // `Uploaded` status and becomes `Ready` once its pages are processed.
344
+ // an uploaded state and becomes ready once its pages are processed.
267
345
  const created = await client.templates.create(
268
346
  { filePath: './nda.pdf' }, // or { buffer, fileName: 'nda.pdf' }
269
347
  { name: 'NDA template' },
@@ -279,18 +357,28 @@ const created = await client.templates.create(
279
357
  const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
280
358
  const template = await client.templates.get(created.id); // includes pages[] + default_document_tags
281
359
  await client.templates.update(created.id, { name: 'NDA v2', message: 'Please sign' });
282
- 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)
283
362
  await client.templates.delete(created.id);
284
363
 
285
- // 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');
286
372
  await client.documents.createFromTemplate(
287
373
  templateId,
288
- [{ 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'] }],
289
375
  { name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
290
376
  );
291
377
 
292
378
  // Estimate the cost before creating → ICostEstimate
293
- 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
+ ]);
294
382
  // → { documents: 1, total_credits: 0, document_balance: 67, credit_balance: 0,
295
383
  // has_sufficient_resources: true, blocking_reason: null, breakdown: [], … }
296
384
  ```
@@ -315,12 +403,49 @@ Attach/detach tags on a specific document via `client.documents.listTags / repla
315
403
 
316
404
  ### Workspaces
317
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
+
318
410
  ```ts
319
- 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 }
320
420
  await client.workspaces.list();
321
421
  await client.workspaces.get(accountId);
322
- 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
+
323
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 });
324
449
  ```
325
450
 
326
451
  ### Field definitions
@@ -355,8 +480,14 @@ await client.fields.listTypes();
355
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.
356
481
 
357
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
+
358
488
  const { access_token, user, accounts } = await client.auth.login('me@example.com', 'pw');
359
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' });
360
491
 
361
492
  // Personal API key
362
493
  await client.auth.createApiKey('current-password');
@@ -369,6 +500,23 @@ await client.auth.requestPasswordReset('me@example.com');
369
500
  await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });
370
501
  ```
371
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
+
372
520
  ### Webhooks
373
521
 
374
522
  ```ts
@@ -386,8 +534,7 @@ await client.webhooks.register({
386
534
  });
387
535
 
388
536
  await client.webhooks.get(); // current subscription or null
389
- await client.webhooks.inactivate();
390
- await client.webhooks.delete();
537
+ await client.webhooks.inactivate(); // stop deliveries (no delete route exists)
391
538
  await client.webhooks.listEventTypes();
392
539
  await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
393
540
  await client.webhooks.retryDispatch(dispatchId);
@@ -395,22 +542,35 @@ await client.webhooks.retryDispatch(dispatchId);
395
542
 
396
543
  ### Webhook verification
397
544
 
398
- 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.
399
551
 
400
552
  ```ts
401
553
  import express from 'express';
402
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
+
403
563
  app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
404
- const signature = req.header('x-assinafy-signature') ?? '';
564
+ const signature = req.header(signatureHeader) ?? '';
405
565
  const rawBody = req.body as Buffer;
406
566
 
407
- if (!client.webhookVerifier.verify(rawBody, signature)) {
567
+ if (!webhookClient.webhookVerifier.verify(rawBody, signature)) {
408
568
  return res.status(401).send('Invalid signature');
409
569
  }
410
570
 
411
- const event = client.webhookVerifier.extractEvent(rawBody);
412
- const type = client.webhookVerifier.getEventType(event);
413
- 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);
414
574
 
415
575
  switch (type) {
416
576
  case 'document_ready': break;
@@ -424,7 +584,10 @@ app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req,
424
584
 
425
585
  ### Signer-side endpoints
426
586
 
427
- 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.
428
591
 
429
592
  ```ts
430
593
  await client.signerDocuments.self(accessCode);
@@ -432,17 +595,20 @@ await client.signerDocuments.acceptTerms(accessCode);
432
595
  await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
433
596
 
434
597
  await client.signerDocuments.getCurrent(signerId, accessCode);
435
- const { data } = await client.signerDocuments.list(signerId, accessCode, { search: 'invoice' });
436
- await client.signerDocuments.download(signerId, documentId, 'original', accessCode);
598
+ const { data } = await client.signerDocuments.list(signerId, accessCode, { per_page: 20 });
599
+ // Signer-side counterpart of documents.search(), authorised by the access code.
600
+ const found = await client.signerDocuments.search(signerId, accessCode, 'invoice');
601
+ await client.signerDocuments.download(signerId, documentId, 'original');
437
602
 
438
603
  await client.signerDocuments.confirmData(documentId, accessCode, {
439
604
  email: 'me@example.com',
440
- whatsapp_phone_number: '+5548999990000',
441
- has_accepted_terms: true,
605
+ full_name: 'Example Signer',
606
+ government_id: '123.456.789-00',
442
607
  });
608
+ await client.signerDocuments.acceptTerms(accessCode);
443
609
 
444
- // Signature image management
445
- 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 });
446
612
  await client.signerDocuments.downloadSignature(accessCode, 'signature');
447
613
 
448
614
  // Sign / decline
@@ -474,14 +640,21 @@ const result = await client.uploadAndRequestSignatures({
474
640
  expiresAt: '2026-12-31T00:00:00Z',
475
641
  });
476
642
 
477
- result.document; // IDocumentUploadResponse
643
+ result.document; // fully-processed IDocumentDetailsResponse (waitForReady: true, the default);
644
+ // the raw IDocumentUploadResponse when waitForReady: false
478
645
  result.assignment; // IAssignment
479
646
  result.signer_ids; // string[]
480
647
  ```
481
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
+
482
654
  ## Errors
483
655
 
484
- 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.
485
658
 
486
659
  ```ts
487
660
  import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
@@ -503,25 +676,52 @@ try {
503
676
 
504
677
  ## Live smoke test
505
678
 
506
- 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.
507
692
 
508
693
  ```bash
509
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts # read-only
510
- ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write # also creates+deletes a signer
511
- 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
512
705
  ```
513
706
 
514
- Set `ASSINAFY_BASE_URL=https://sandbox.assinafy.com.br/v1` to run it against the
515
- 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.
516
713
 
517
714
  ## Development
518
715
 
519
716
  ```bash
520
- bun install # or npm install
521
- bun test # runs bun:test suites (Bun is required for tests)
522
- npm run typecheck # tsc --noEmit
523
- npm run lint
524
- 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
525
725
  ```
526
726
 
527
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.