@assinafy/sdk 1.4.0

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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Assinafy
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,466 @@
1
+ # @assinafy/sdk
2
+
3
+ TypeScript SDK for the [Assinafy API](https://api.assinafy.com.br/v1/docs) — a Brazilian digital signature platform.
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.
6
+
7
+ ## Requirements
8
+
9
+ - Node.js 18+ for the built-in `FormData` / `Blob` APIs used by uploads
10
+ - or Bun 1.0+
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ npm install @assinafy/sdk
16
+ # or
17
+ bun add @assinafy/sdk
18
+ ```
19
+
20
+ The package is published to both [npmjs.com](https://www.npmjs.com/package/@assinafy/sdk) and [GitHub Packages](https://github.com/assinafy/typescript-sdk/packages). To install from GitHub Packages, add to your `.npmrc`:
21
+
22
+ ```
23
+ @assinafy:registry=https://npm.pkg.github.com
24
+ //npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}
25
+ ```
26
+
27
+ ## Quick start
28
+
29
+ ```ts
30
+ import { AssinafyClient } from '@assinafy/sdk';
31
+
32
+ const client = new AssinafyClient({
33
+ apiKey: process.env.ASSINAFY_API_KEY!,
34
+ accountId: process.env.ASSINAFY_ACCOUNT_ID!,
35
+ webhookSecret: process.env.ASSINAFY_WEBHOOK_SECRET,
36
+ });
37
+
38
+ const result = await client.uploadAndRequestSignatures({
39
+ source: { filePath: './contract.pdf' },
40
+ signers: [
41
+ { name: 'John Doe', email: 'john@example.com' },
42
+ { name: 'Jane Smith', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
43
+ ],
44
+ message: 'Please sign this contract',
45
+ });
46
+
47
+ console.log('Document ID:', result.document.id);
48
+ ```
49
+
50
+ ## Authentication
51
+
52
+ The API supports two authentication methods. Prefer `apiKey` — it maps to the `X-Api-Key` header recommended by Assinafy for backend services.
53
+
54
+ ```ts
55
+ // Preferred: X-Api-Key header
56
+ new AssinafyClient({ apiKey: 'k_xxx', accountId: 'acc_xxx' });
57
+
58
+ // Legacy: Authorization: Bearer <token>
59
+ new AssinafyClient({ token: 'jwt_xxx', accountId: 'acc_xxx' });
60
+ ```
61
+
62
+ ## Configuration
63
+
64
+ | Option | Type | Default | Description |
65
+ | --------------- | -------- | --------------------------------------- | --------------------------------------------- |
66
+ | `apiKey` | string | — | Preferred credential (sent as `X-Api-Key`). |
67
+ | `token` | string | — | Legacy access token (sent as `Bearer`). |
68
+ | `accountId` | string | — | Default workspace/account ID. |
69
+ | `baseUrl` | string | `https://api.assinafy.com.br/v1` | Override base URL. |
70
+ | `webhookSecret` | string | — | Shared secret used by `WebhookVerifier`. |
71
+ | `timeout` | number | `30000` | Request timeout in milliseconds. |
72
+ | `logger` | `Logger` | no-op | Optional `{debug,info,warn,error}` logger. |
73
+
74
+ ### Factories
75
+
76
+ ```ts
77
+ // Positional factory
78
+ const client = AssinafyClient.create('api-key', 'account-id', { webhookSecret: 'shhh' });
79
+
80
+ // From a plain object (accepts snake_case or camelCase keys)
81
+ const client = AssinafyClient.fromConfig({
82
+ api_key: process.env.ASSINAFY_API_KEY!,
83
+ account_id: process.env.ASSINAFY_ACCOUNT_ID!,
84
+ });
85
+ ```
86
+
87
+ ## Endpoint coverage
88
+
89
+ Every public endpoint documented in https://api.assinafy.com.br/v1/docs is covered. The table below maps each resource to its API surface.
90
+
91
+ | Resource | Endpoints |
92
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | `client.documents` | list, upload, details, activities, waitUntilReady, download, thumbnail, downloadPage, statuses, delete, verify, createFromTemplate, estimateCostFromTemplate, **getPublic**, **sendToken**, **listTags**, **replaceTags**, **addTags**, **detachTag**, isFullySigned, getSigningProgress |
94
+ | `client.signers` | create, get, list, update, delete, findByEmail |
95
+ | `client.assignments` | create, estimateCost, resetExpiration, resendNotification, estimateResendCost, listWhatsAppNotifications, cancel |
96
+ | `client.templates` | list, get, downloadPage |
97
+ | `client.tags` | list, create, update, delete |
98
+ | `client.workspaces` | create, list, get, update, delete |
99
+ | `client.webhooks` | register, get, inactivate, delete, listEventTypes, listDispatches, retryDispatch |
100
+ | `client.fields` | create, list, get, update, delete, validate, validateMultiple, listTypes |
101
+ | `client.auth` | login, socialLogin, createApiKey, getApiKey, deleteApiKey, changePassword, requestPasswordReset, resetPassword |
102
+ | `client.signerDocuments` | getCurrent, list, download, signMultiple, declineMultiple, self, acceptTerms, verifyEmail, confirmData, uploadSignature, downloadSignature, getAssignment, sign, decline |
103
+ | `client.webhookVerifier` | verify, extractEvent, getEventType, getEventData |
104
+
105
+ ## Resources
106
+
107
+ Most account-scoped methods accept an optional `accountId` that overrides the client default. Workspace `get/update/delete` always require an explicit account ID.
108
+
109
+ ### Documents
110
+
111
+ ```ts
112
+ // Upload from a file path (recommended)
113
+ const doc = await client.documents.upload(
114
+ { filePath: './contract.pdf' },
115
+ { metadata: { type: 'service' } },
116
+ );
117
+
118
+ // …or from a Buffer already in memory
119
+ await client.documents.upload({ buffer, fileName: 'contract.pdf' });
120
+
121
+ const { data, meta } = await client.documents.list({ page: 1, per_page: 20, sort: '-created_at' });
122
+ await client.documents.details(doc.id);
123
+ await client.documents.activities(doc.id);
124
+ await client.documents.waitUntilReady(doc.id, { maxWaitMs: 30_000 });
125
+
126
+ await client.documents.download(doc.id, 'certificated'); // 'original' | 'certificated' | 'certificate-page' | 'bundle'
127
+ await client.documents.thumbnail(doc.id);
128
+ await client.documents.downloadPage(doc.id, pageId);
129
+
130
+ await client.documents.statuses(); // list every status code + deletable flag
131
+ await client.documents.isFullySigned(doc.id);
132
+ await client.documents.getSigningProgress(doc.id);
133
+ await client.documents.delete(doc.id);
134
+
135
+ // Verify a signed document by its SHA-1 hash
136
+ await client.documents.verify('FE32EDDADE7CBDDCBB934E7402047450B0E59C02');
137
+
138
+ // Public endpoints (no auth)
139
+ await client.documents.getPublic(doc.id);
140
+ await client.documents.sendToken(doc.id, 'jane@example.com', 'email');
141
+
142
+ // Tags attached to a document (by tag name; unknown names are auto-created)
143
+ await client.documents.listTags(doc.id);
144
+ await client.documents.replaceTags(doc.id, ['Contracts', '2026-Q1']); // [] detaches all
145
+ await client.documents.addTags(doc.id, ['Urgent']); // append, idempotent
146
+ await client.documents.detachTag(doc.id, tagId); // remove one
147
+ ```
148
+
149
+ Uploads are validated locally: only `.pdf` files up to 25 MB are accepted (the API's current hard limit).
150
+
151
+ List endpoints return `{ data, meta }` where `meta` is populated from the `X-Pagination-*` headers returned by the API.
152
+
153
+ ### Signers
154
+
155
+ ```ts
156
+ await client.signers.create({
157
+ full_name: 'John Doe',
158
+ email: 'john@example.com',
159
+ whatsapp_phone_number: '+5548999990000',
160
+ cpf: '123.456.789-00', // optional Brazilian tax ID — non-digits are stripped automatically
161
+ });
162
+
163
+ // `email` is optional — a WhatsApp-only signer is valid (at least one is required)
164
+ await client.signers.create({
165
+ full_name: 'WhatsApp Only',
166
+ whatsapp_phone_number: '+5548999990000',
167
+ });
168
+
169
+ // PHP SDK compatibility aliases are also accepted
170
+ await client.signers.create({
171
+ full_name: 'Jane Doe',
172
+ email: 'jane@example.com',
173
+ phone: '+5548999991111', // alias for whatsapp_phone_number
174
+ });
175
+
176
+ await client.signers.get(signerId);
177
+ await client.signers.list({ page: 1, per_page: 50, search: 'john' });
178
+ await client.signers.update(signerId, { full_name: 'Johnny Doe' });
179
+ await client.signers.delete(signerId);
180
+
181
+ const existing = await client.signers.findByEmail('john@example.com');
182
+ ```
183
+
184
+ When an `email` is supplied, `signers.create()` is idempotent by email, matching the PHP SDK behavior: it reuses an existing signer when the same email is already present in the workspace. WhatsApp-only signers (no email) are always created fresh.
185
+
186
+ ### Assignments
187
+
188
+ ```ts
189
+ // Signers may be ids or objects — the SDK normalises to the API shape.
190
+ await client.assignments.create(documentId, {
191
+ method: 'virtual',
192
+ signers: ['signer-1', 'signer-2'],
193
+ message: 'Please review and sign',
194
+ expires_at: '2024-12-31T23:59:00Z',
195
+ copy_receivers: ['observer-id'],
196
+ });
197
+
198
+ // Sequential signing: `step` controls signing order (parallel within a step).
199
+ await client.assignments.create(documentId, {
200
+ method: 'virtual',
201
+ signers: [
202
+ { id: 'signer-1', step: 1 },
203
+ { id: 'signer-2', step: 2 }, // notified only after step 1 finishes
204
+ ],
205
+ });
206
+
207
+ // Estimate cost (signers may omit `id` when only the channel matters)
208
+ await client.assignments.estimateCost(documentId, { signers: ['signer-1'] });
209
+ await client.assignments.estimateCost(documentId, {
210
+ signers: [{ verification_method: 'Whatsapp' }],
211
+ });
212
+
213
+ await client.assignments.resetExpiration(documentId, assignmentId, '2025-06-30T00:00:00Z');
214
+ await client.assignments.resetExpiration(documentId, assignmentId, null); // remove expiration
215
+ await client.assignments.resendNotification(documentId, assignmentId, signerId);
216
+ await client.assignments.estimateResendCost(documentId, assignmentId, signerId);
217
+ await client.assignments.listWhatsAppNotifications(documentId, assignmentId);
218
+ await client.assignments.cancel(documentId, 'No longer needed');
219
+ ```
220
+
221
+ For backwards compatibility, the SDK also accepts legacy `signer_ids` and `signerIds` payloads and rewrites them to the current `signers: [{ id }]` format expected by the API.
222
+
223
+ ### Templates
224
+
225
+ ```ts
226
+ const { data, meta } = await client.templates.list({ search: 'NDA', per_page: 20 });
227
+ const template = await client.templates.get(templateId);
228
+ await client.templates.downloadPage(templateId, pageId);
229
+
230
+ // Create a document from a template (each signer maps to a template role)
231
+ await client.documents.createFromTemplate(
232
+ templateId,
233
+ [{ role_id: template.roles![0].id, id: signerId, verification_method: 'Email', notification_methods: ['Email'] }],
234
+ { name: 'NDA - John Doe', message: 'Please sign at your earliest convenience.' },
235
+ );
236
+
237
+ // Estimate the cost before creating
238
+ await client.documents.estimateCostFromTemplate(templateId, [{ role_id: 'role_id', id: signerId }]);
239
+ ```
240
+
241
+ ### Tags
242
+
243
+ Workspace-scoped labels that can be attached to documents and templates. Tag names are unique per workspace (case-insensitive).
244
+
245
+ ```ts
246
+ await client.tags.list({ search: 'contract' }); // ITag[]
247
+ const tag = await client.tags.create({ name: 'Contracts', color: 'ff8800' });
248
+ await client.tags.update(tag.id, { name: 'Sales Contracts' });
249
+ await client.tags.update(tag.id, { color: null }); // clear the color
250
+ await client.tags.delete(tag.id); // 409 if still attached
251
+ await client.tags.delete(tag.id, { force: true }); // detach everywhere, then delete
252
+ ```
253
+
254
+ Attach/detach tags on a specific document via `client.documents.listTags / replaceTags / addTags / detachTag` (see [Documents](#documents)).
255
+
256
+ ### Workspaces
257
+
258
+ ```ts
259
+ await client.workspaces.create({ name: 'My Workspace', primary_color: '#ff0066' });
260
+ await client.workspaces.list();
261
+ await client.workspaces.get(accountId);
262
+ await client.workspaces.update(accountId, { name: 'Renamed' });
263
+ await client.workspaces.delete(accountId);
264
+ ```
265
+
266
+ ### Field definitions
267
+
268
+ Custom field types used by `collect`-method assignments.
269
+
270
+ ```ts
271
+ await client.fields.create({ type: 'text', name: 'Contract Number' });
272
+ await client.fields.list({ include_inactive: true, include_standard: true });
273
+ await client.fields.get(fieldId);
274
+ await client.fields.update(fieldId, { name: 'Updated Name' });
275
+ await client.fields.delete(fieldId);
276
+
277
+ // Validate a single value (signer-access-code only required for signer-side calls)
278
+ await client.fields.validate(fieldId, '400.676.228-36', { signerAccessCode });
279
+
280
+ // Validate multiple values at once
281
+ await client.fields.validateMultiple(
282
+ [
283
+ { field_id: 'f1', value: '1111111111111' },
284
+ { field_id: 'f2', value: 'foo@bar.com' },
285
+ ],
286
+ { signerAccessCode },
287
+ );
288
+
289
+ // Catalog of every field type the platform recognises
290
+ await client.fields.listTypes();
291
+ ```
292
+
293
+ ### Authentication / API key management
294
+
295
+ 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.
296
+
297
+ ```ts
298
+ const { access_token, user, accounts } = await client.auth.login('me@example.com', 'pw');
299
+ await client.auth.socialLogin({ provider: 'google', token: 'google-id-token', has_accepted_terms: true });
300
+
301
+ // Personal API key
302
+ await client.auth.createApiKey('current-password');
303
+ await client.auth.getApiKey(); // → { api_key: '****...nBNr' } or null
304
+ await client.auth.deleteApiKey();
305
+
306
+ // Password lifecycle
307
+ await client.auth.changePassword({ email, password: 'current', new_password: 'next' });
308
+ await client.auth.requestPasswordReset('me@example.com');
309
+ await client.auth.resetPassword({ email, token: 'tk', new_password: 'next' });
310
+ ```
311
+
312
+ ### Webhooks
313
+
314
+ ```ts
315
+ await client.webhooks.register({
316
+ url: 'https://example.com/webhooks/assinafy',
317
+ email: 'admin@example.com',
318
+ // events defaults to the current SDK default set below
319
+ events: [
320
+ 'document_ready',
321
+ 'document_prepared',
322
+ 'signer_signed_document',
323
+ 'signer_rejected_document',
324
+ 'document_processing_failed',
325
+ ],
326
+ });
327
+
328
+ await client.webhooks.get(); // current subscription or null
329
+ await client.webhooks.inactivate();
330
+ await client.webhooks.delete();
331
+ await client.webhooks.listEventTypes();
332
+ await client.webhooks.listDispatches({ delivered: false, page: 1, 'per-page': 20 });
333
+ await client.webhooks.retryDispatch(dispatchId);
334
+ ```
335
+
336
+ ### Webhook verification
337
+
338
+ 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.
339
+
340
+ ```ts
341
+ import express from 'express';
342
+
343
+ app.post('/webhooks/assinafy', express.raw({ type: 'application/json' }), (req, res) => {
344
+ const signature = req.header('x-assinafy-signature') ?? '';
345
+ const rawBody = req.body as Buffer;
346
+
347
+ if (!client.webhookVerifier.verify(rawBody, signature)) {
348
+ return res.status(401).send('Invalid signature');
349
+ }
350
+
351
+ const event = client.webhookVerifier.extractEvent(rawBody);
352
+ const type = client.webhookVerifier.getEventType(event);
353
+ const data = client.webhookVerifier.getEventData(event);
354
+
355
+ switch (type) {
356
+ case 'document_ready': break;
357
+ case 'signer_signed_document': break;
358
+ case 'signer_rejected_document': break;
359
+ case 'document_processing_failed':break;
360
+ }
361
+ res.sendStatus(200);
362
+ });
363
+ ```
364
+
365
+ ### Signer-side endpoints
366
+
367
+ For building custom signer portals. Every call requires the `signer-access-code` URL parameter that Assinafy emails/whatsapps to the signer.
368
+
369
+ ```ts
370
+ await client.signerDocuments.self(accessCode);
371
+ await client.signerDocuments.acceptTerms(accessCode);
372
+ await client.signerDocuments.verifyEmail({ signerAccessCode: accessCode, verificationCode: '123456' });
373
+
374
+ await client.signerDocuments.getCurrent(signerId, accessCode);
375
+ const { data } = await client.signerDocuments.list(signerId, accessCode, { search: 'invoice' });
376
+ await client.signerDocuments.download(signerId, documentId, 'original', accessCode);
377
+
378
+ await client.signerDocuments.confirmData(documentId, accessCode, {
379
+ email: 'me@example.com',
380
+ whatsapp_phone_number: '+5548999990000',
381
+ has_accepted_terms: true,
382
+ });
383
+
384
+ // Signature image management
385
+ await client.signerDocuments.uploadSignature(accessCode, pngBuffer, { imageType: 'signature' });
386
+ await client.signerDocuments.downloadSignature(accessCode, 'signature');
387
+
388
+ // Sign / decline
389
+ const assignment = await client.signerDocuments.getAssignment(accessCode);
390
+ await client.signerDocuments.sign(documentId, assignmentId, accessCode, [
391
+ { itemId, fieldId, pageId, value: 'Signed by John' },
392
+ ]);
393
+ await client.signerDocuments.decline(documentId, assignmentId, accessCode, 'Not authorized');
394
+
395
+ // Bulk operations
396
+ await client.signerDocuments.signMultiple(['doc-1', 'doc-2'], accessCode);
397
+ await client.signerDocuments.declineMultiple(['doc-1'], 'Unfavorable terms', accessCode);
398
+ ```
399
+
400
+ ## High-level helper
401
+
402
+ Uploads a PDF, waits for processing, reuses or creates signers by email, and kicks off a virtual assignment.
403
+
404
+ ```ts
405
+ const result = await client.uploadAndRequestSignatures({
406
+ source: { filePath: './contract.pdf' },
407
+ signers: [
408
+ { name: 'John', email: 'john@example.com' },
409
+ { name: 'Jane', email: 'jane@example.com', whatsapp_phone_number: '+5548999990000' },
410
+ ],
411
+ message: 'Please sign',
412
+ metadata: { year: 2026 },
413
+ waitForReady: true,
414
+ expiresAt: '2026-12-31T00:00:00Z',
415
+ });
416
+
417
+ result.document; // IDocumentUploadResponse
418
+ result.assignment; // IAssignment
419
+ result.signer_ids; // string[]
420
+ ```
421
+
422
+ ## Errors
423
+
424
+ Every method rejects with an `AssinafyError` subclass.
425
+
426
+ ```ts
427
+ import { ApiError, ValidationError, NetworkError, AssinafyError } from '@assinafy/sdk';
428
+
429
+ try {
430
+ await client.documents.upload({ filePath: './x.pdf' });
431
+ } catch (err) {
432
+ if (err instanceof ValidationError) {
433
+ console.error('Validation failed:', err.errors);
434
+ } else if (err instanceof ApiError) {
435
+ console.error(`API error ${err.statusCode}:`, err.responseData);
436
+ } else if (err instanceof NetworkError) {
437
+ console.error('Network error:', err.message);
438
+ } else if (err instanceof AssinafyError) {
439
+ console.error('SDK error:', err.message, err.context);
440
+ }
441
+ }
442
+ ```
443
+
444
+ ## Live smoke test
445
+
446
+ 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.
447
+
448
+ ```bash
449
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts # read-only
450
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --write # also creates+deletes a signer
451
+ ASSINAFY_API_KEY=… ASSINAFY_ACCOUNT_ID=… bun scripts/live-smoke.ts --upload # also uploads+deletes a PDF
452
+ ```
453
+
454
+ ## Development
455
+
456
+ ```bash
457
+ bun install # or npm install
458
+ bun test # runs bun:test suites (Bun is required for tests)
459
+ npm run typecheck # tsc --noEmit
460
+ npm run lint
461
+ npm run build # tsup → dist/ (CJS + ESM + .d.ts)
462
+ ```
463
+
464
+ ## License
465
+
466
+ MIT