@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/CHANGELOG.md +469 -0
- package/README.md +267 -67
- package/SECURITY.md +59 -0
- package/dist/index.d.mts +4141 -467
- package/dist/index.d.ts +4141 -467
- package/dist/index.js +4276 -415
- package/dist/index.mjs +4274 -415
- package/docs/API_COVERAGE.md +209 -0
- package/docs/COMPATIBILITY.md +285 -0
- package/docs/RELEASING.md +138 -0
- package/package.json +40 -19
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
|
-
|
|
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
|
|
10
|
-
|
|
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
|
-
//
|
|
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 | — |
|
|
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 | — |
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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'
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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, [
|
|
162
|
-
await client.documents.addTags(doc.id, [
|
|
163
|
-
await client.documents.detachTag(doc.id,
|
|
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
|
|
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
|
-
//
|
|
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 (
|
|
228
|
-
await client.assignments.estimateCost(documentId, { signers: [
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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:
|
|
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, [
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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(
|
|
564
|
+
const signature = req.header(signatureHeader) ?? '';
|
|
405
565
|
const rawBody = req.body as Buffer;
|
|
406
566
|
|
|
407
|
-
if (!
|
|
567
|
+
if (!webhookClient.webhookVerifier.verify(rawBody, signature)) {
|
|
408
568
|
return res.status(401).send('Invalid signature');
|
|
409
569
|
}
|
|
410
570
|
|
|
411
|
-
const event =
|
|
412
|
-
const type =
|
|
413
|
-
const data =
|
|
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.
|
|
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, {
|
|
436
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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; //
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=…
|
|
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
|
-
|
|
515
|
-
|
|
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
|
|
521
|
-
bun
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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.
|