@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/CHANGELOG.md +469 -0
- package/README.md +240 -63
- package/SECURITY.md +59 -0
- package/dist/index.d.mts +3845 -495
- package/dist/index.d.ts +3845 -495
- package/dist/index.js +3931 -364
- package/dist/index.mjs +3930 -364
- package/docs/API_COVERAGE.md +209 -0
- package/docs/COMPATIBILITY.md +285 -0
- package/docs/RELEASING.md +138 -0
- package/package.json +27 -15
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
|
|
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
|
-
|
|
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.
|
|
12
|
-
on 22 (maintenance LTS)
|
|
13
|
-
April 2026 and is
|
|
14
|
-
- or Bun 1.
|
|
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
|
-
//
|
|
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 | — |
|
|
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 | — |
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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, [
|
|
180
|
-
await client.documents.addTags(doc.id, [
|
|
181
|
-
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
|
|
182
226
|
```
|
|
183
227
|
|
|
184
|
-
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).
|
|
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
|
-
//
|
|
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 (
|
|
250
|
-
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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:
|
|
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, [
|
|
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
|
-
|
|
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, {
|
|
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
|
-
|
|
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(
|
|
564
|
+
const signature = req.header(signatureHeader) ?? '';
|
|
426
565
|
const rawBody = req.body as Buffer;
|
|
427
566
|
|
|
428
|
-
if (!
|
|
567
|
+
if (!webhookClient.webhookVerifier.verify(rawBody, signature)) {
|
|
429
568
|
return res.status(401).send('Invalid signature');
|
|
430
569
|
}
|
|
431
570
|
|
|
432
|
-
const event =
|
|
433
|
-
const type =
|
|
434
|
-
const data =
|
|
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.
|
|
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, {
|
|
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'
|
|
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
|
-
|
|
464
|
-
|
|
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; //
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
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
|
|
535
705
|
```
|
|
536
706
|
|
|
537
|
-
|
|
538
|
-
|
|
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
|
|
544
|
-
bun
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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.
|