@assinafy/sdk 1.5.0 → 2.0.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/README.md +32 -9
- package/dist/index.d.mts +368 -44
- package/dist/index.d.ts +368 -44
- package/dist/index.js +396 -102
- package/dist/index.mjs +395 -102
- package/package.json +18 -9
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ __export(index_exports, {
|
|
|
38
38
|
DEFAULT_WEBHOOK_EVENTS: () => DEFAULT_WEBHOOK_EVENTS,
|
|
39
39
|
DocumentResource: () => DocumentResource,
|
|
40
40
|
FieldsResource: () => FieldsResource,
|
|
41
|
+
MAX_UPLOAD_BYTES: () => MAX_UPLOAD_BYTES,
|
|
41
42
|
NetworkError: () => NetworkError,
|
|
42
43
|
SignerDocumentsResource: () => SignerDocumentsResource,
|
|
43
44
|
SignerResource: () => SignerResource,
|
|
@@ -56,16 +57,16 @@ var import_axios2 = __toESM(require("axios"));
|
|
|
56
57
|
|
|
57
58
|
// src/errors.ts
|
|
58
59
|
var AssinafyError = class extends Error {
|
|
60
|
+
context;
|
|
59
61
|
constructor(message, context = {}, options) {
|
|
60
|
-
super(message);
|
|
62
|
+
super(message, options);
|
|
61
63
|
this.name = "AssinafyError";
|
|
62
64
|
this.context = context;
|
|
63
|
-
if (options?.cause !== void 0) {
|
|
64
|
-
this.cause = options.cause;
|
|
65
|
-
}
|
|
66
65
|
}
|
|
67
66
|
};
|
|
68
67
|
var ApiError = class _ApiError extends AssinafyError {
|
|
68
|
+
statusCode;
|
|
69
|
+
responseData;
|
|
69
70
|
constructor(message, statusCode, responseData = null, options) {
|
|
70
71
|
super(message, { statusCode, responseData }, options);
|
|
71
72
|
this.name = "ApiError";
|
|
@@ -81,6 +82,7 @@ var ApiError = class _ApiError extends AssinafyError {
|
|
|
81
82
|
}
|
|
82
83
|
};
|
|
83
84
|
var ValidationError = class extends AssinafyError {
|
|
85
|
+
errors;
|
|
84
86
|
constructor(message = "Validation failed", errors = {}) {
|
|
85
87
|
super(message, { errors });
|
|
86
88
|
this.name = "ValidationError";
|
|
@@ -106,6 +108,23 @@ function handleAssinafyResponse(response) {
|
|
|
106
108
|
}
|
|
107
109
|
return response;
|
|
108
110
|
}
|
|
111
|
+
function decodeBinaryErrorBody(data) {
|
|
112
|
+
let text;
|
|
113
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
114
|
+
text = data.toString("utf8");
|
|
115
|
+
} else if (data instanceof ArrayBuffer) {
|
|
116
|
+
text = Buffer.from(data).toString("utf8");
|
|
117
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
118
|
+
text = Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
119
|
+
} else {
|
|
120
|
+
return data;
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
return JSON.parse(text);
|
|
124
|
+
} catch {
|
|
125
|
+
return text.length > 0 ? { message: text } : null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
109
128
|
function toSdkError(error, fallbackMessage) {
|
|
110
129
|
if (error instanceof AssinafyError) {
|
|
111
130
|
return error;
|
|
@@ -113,14 +132,15 @@ function toSdkError(error, fallbackMessage) {
|
|
|
113
132
|
if (import_axios.default.isAxiosError(error)) {
|
|
114
133
|
const status = error.response?.status;
|
|
115
134
|
if (status) {
|
|
116
|
-
|
|
135
|
+
const body = decodeBinaryErrorBody(error.response?.data ?? null);
|
|
136
|
+
return ApiError.fromResponse(status, body ?? null);
|
|
117
137
|
}
|
|
118
138
|
return new NetworkError(`${fallbackMessage}: ${error.message}`, { cause: error });
|
|
119
139
|
}
|
|
120
140
|
if (error instanceof Error) {
|
|
121
141
|
return new AssinafyError(`${fallbackMessage}: ${error.message}`, {}, { cause: error });
|
|
122
142
|
}
|
|
123
|
-
return new AssinafyError(fallbackMessage, { cause: error });
|
|
143
|
+
return new AssinafyError(fallbackMessage, {}, { cause: error });
|
|
124
144
|
}
|
|
125
145
|
function createNoopLogger() {
|
|
126
146
|
return {
|
|
@@ -139,6 +159,14 @@ function cleanParams(params) {
|
|
|
139
159
|
}
|
|
140
160
|
return out;
|
|
141
161
|
}
|
|
162
|
+
function cleanListParams(params) {
|
|
163
|
+
const out = cleanParams(params);
|
|
164
|
+
if (out["per_page"] !== void 0) {
|
|
165
|
+
out["per-page"] ??= out["per_page"];
|
|
166
|
+
delete out["per_page"];
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
142
170
|
|
|
143
171
|
// src/support/retry.ts
|
|
144
172
|
function header(headers, name) {
|
|
@@ -175,7 +203,57 @@ function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
|
|
|
175
203
|
return Math.min(backoffMs(attempt), maxDelayMs);
|
|
176
204
|
}
|
|
177
205
|
|
|
206
|
+
// src/resources/upload.ts
|
|
207
|
+
var import_node_fs = require("fs");
|
|
208
|
+
var import_node_path = __toESM(require("path"));
|
|
209
|
+
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
210
|
+
async function loadSource(source) {
|
|
211
|
+
if ("buffer" in source) {
|
|
212
|
+
if (!source.fileName) {
|
|
213
|
+
throw new ValidationError("fileName is required when uploading a Buffer");
|
|
214
|
+
}
|
|
215
|
+
return { buffer: source.buffer, fileName: source.fileName };
|
|
216
|
+
}
|
|
217
|
+
if (!source.filePath) {
|
|
218
|
+
throw new ValidationError("filePath is required");
|
|
219
|
+
}
|
|
220
|
+
const buffer = await import_node_fs.promises.readFile(source.filePath);
|
|
221
|
+
return { buffer, fileName: source.fileName ?? import_node_path.default.basename(source.filePath) };
|
|
222
|
+
}
|
|
223
|
+
function validateUpload(buffer, fileName) {
|
|
224
|
+
if (!buffer || buffer.byteLength === 0) {
|
|
225
|
+
throw new ValidationError("File buffer is empty", { fileName });
|
|
226
|
+
}
|
|
227
|
+
if (!fileName.toLowerCase().endsWith(".pdf")) {
|
|
228
|
+
throw new ValidationError("Only PDF files are supported", { fileName });
|
|
229
|
+
}
|
|
230
|
+
if (buffer.byteLength > MAX_UPLOAD_BYTES) {
|
|
231
|
+
throw new ValidationError("File size exceeds maximum allowed (25MB)", {
|
|
232
|
+
fileSize: buffer.byteLength,
|
|
233
|
+
maxSize: MAX_UPLOAD_BYTES
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function toUploadFileName(name) {
|
|
238
|
+
return name.toLowerCase().endsWith(".pdf") ? name : `${name}.pdf`;
|
|
239
|
+
}
|
|
240
|
+
function buildUploadForm(buffer, fileName, options = {}) {
|
|
241
|
+
const form = new FormData();
|
|
242
|
+
const view = new Uint8Array(
|
|
243
|
+
buffer.buffer,
|
|
244
|
+
buffer.byteOffset,
|
|
245
|
+
buffer.byteLength
|
|
246
|
+
);
|
|
247
|
+
const partName = options.name === void 0 ? fileName : toUploadFileName(options.name);
|
|
248
|
+
form.append("file", new Blob([view], { type: "application/pdf" }), partName);
|
|
249
|
+
if (options.metadata) {
|
|
250
|
+
form.append("metadata", JSON.stringify(options.metadata));
|
|
251
|
+
}
|
|
252
|
+
return form;
|
|
253
|
+
}
|
|
254
|
+
|
|
178
255
|
// src/resources/base.ts
|
|
256
|
+
var MULTIPART_CONTENT_TYPE = "multipart/form-data";
|
|
179
257
|
var BaseResource = class {
|
|
180
258
|
constructor(http, defaultAccountId, logger = createNoopLogger()) {
|
|
181
259
|
this.http = http;
|
|
@@ -237,6 +315,35 @@ var BaseResource = class {
|
|
|
237
315
|
throw toSdkError(err, label);
|
|
238
316
|
}
|
|
239
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* Upload a PDF as `multipart/form-data` and assert the API echoed an id.
|
|
320
|
+
*
|
|
321
|
+
* Shared by `documents.upload` and `templates.create`, which are the same
|
|
322
|
+
* sequence over different paths: load → validate → build form → POST →
|
|
323
|
+
* assert an id came back. Callers keep their own success logging.
|
|
324
|
+
*
|
|
325
|
+
* @param path - Account-scoped endpoint to POST to.
|
|
326
|
+
* @param source - The PDF, as a file path or in-memory buffer.
|
|
327
|
+
* @param formOptions - `name` (display name) and optional `metadata`.
|
|
328
|
+
* @param labels - `errorLabel` for the request failure, `missingId` for a
|
|
329
|
+
* `2xx` that returned no id.
|
|
330
|
+
*/
|
|
331
|
+
async uploadPdf(path2, source, formOptions, labels) {
|
|
332
|
+
const { buffer, fileName } = await loadSource(source);
|
|
333
|
+
validateUpload(buffer, fileName);
|
|
334
|
+
this.logger.info("Uploading PDF", { path: path2, fileName, size: buffer.byteLength });
|
|
335
|
+
const form = buildUploadForm(buffer, fileName, formOptions);
|
|
336
|
+
const result = await this.call(
|
|
337
|
+
labels.errorLabel,
|
|
338
|
+
() => this.http.post(path2, form, { headers: { "Content-Type": MULTIPART_CONTENT_TYPE } })
|
|
339
|
+
);
|
|
340
|
+
if (!result?.id) {
|
|
341
|
+
throw new ValidationError(labels.missingId, {
|
|
342
|
+
response: result
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
return result;
|
|
346
|
+
}
|
|
240
347
|
/** Execute a paginated list call and attach meta from `X-Pagination-*` headers. */
|
|
241
348
|
async callList(label, request) {
|
|
242
349
|
try {
|
|
@@ -278,48 +385,6 @@ function toInt(value) {
|
|
|
278
385
|
return Number.isFinite(n) ? n : void 0;
|
|
279
386
|
}
|
|
280
387
|
|
|
281
|
-
// src/resources/upload.ts
|
|
282
|
-
var import_node_fs = require("fs");
|
|
283
|
-
var import_node_path = __toESM(require("path"));
|
|
284
|
-
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
285
|
-
async function loadSource(source) {
|
|
286
|
-
if ("buffer" in source) {
|
|
287
|
-
if (!source.fileName) {
|
|
288
|
-
throw new ValidationError("fileName is required when uploading a Buffer");
|
|
289
|
-
}
|
|
290
|
-
return { buffer: source.buffer, fileName: source.fileName };
|
|
291
|
-
}
|
|
292
|
-
if (!source.filePath) {
|
|
293
|
-
throw new ValidationError("filePath is required");
|
|
294
|
-
}
|
|
295
|
-
const buffer = await import_node_fs.promises.readFile(source.filePath);
|
|
296
|
-
return { buffer, fileName: source.fileName ?? import_node_path.default.basename(source.filePath) };
|
|
297
|
-
}
|
|
298
|
-
function validateUpload(buffer, fileName) {
|
|
299
|
-
if (!buffer || buffer.byteLength === 0) {
|
|
300
|
-
throw new ValidationError("File buffer is empty", { fileName });
|
|
301
|
-
}
|
|
302
|
-
if (!fileName.toLowerCase().endsWith(".pdf")) {
|
|
303
|
-
throw new ValidationError("Only PDF files are supported", { fileName });
|
|
304
|
-
}
|
|
305
|
-
if (buffer.byteLength > MAX_UPLOAD_BYTES) {
|
|
306
|
-
throw new ValidationError("File size exceeds maximum allowed (25MB)", {
|
|
307
|
-
fileSize: buffer.byteLength,
|
|
308
|
-
maxSize: MAX_UPLOAD_BYTES
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
function buildUploadForm(buffer, fileName, options = {}) {
|
|
313
|
-
const form = new FormData();
|
|
314
|
-
const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
315
|
-
form.append("file", new Blob([view], { type: "application/pdf" }), fileName);
|
|
316
|
-
form.append("name", options.name ?? fileName);
|
|
317
|
-
if (options.metadata) {
|
|
318
|
-
form.append("metadata", JSON.stringify(options.metadata));
|
|
319
|
-
}
|
|
320
|
-
return form;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
388
|
// src/resources/documents.ts
|
|
324
389
|
var READY_STATUSES = /* @__PURE__ */ new Set([
|
|
325
390
|
"metadata_ready",
|
|
@@ -334,45 +399,153 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
|
334
399
|
]);
|
|
335
400
|
var DocumentResource = class extends BaseResource {
|
|
336
401
|
/**
|
|
337
|
-
* Upload a PDF to the workspace.
|
|
402
|
+
* Upload a PDF to the workspace (`POST /accounts/{accountId}/documents`).
|
|
403
|
+
*
|
|
404
|
+
* The document is created in `metadata_processing` status and becomes
|
|
405
|
+
* usable once it reaches `metadata_ready`; use
|
|
406
|
+
* {@link DocumentResource.waitUntilReady} to await that transition. Note
|
|
407
|
+
* that {@link DocumentResource.rename} and {@link DocumentResource.delete}
|
|
408
|
+
* return `400` while the document is still processing.
|
|
409
|
+
*
|
|
410
|
+
* @param source - The PDF to upload, as a file path or an in-memory buffer.
|
|
411
|
+
* @param options - Display name, metadata, and account override.
|
|
412
|
+
* @returns The created document. Response shape:
|
|
413
|
+
* ```jsonc
|
|
414
|
+
* {
|
|
415
|
+
* "resource": "document",
|
|
416
|
+
* "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
417
|
+
* "name": "Service agreement.pdf",
|
|
418
|
+
* "status": "metadata_processing",
|
|
419
|
+
* "created_at": "2026-07-15T16:15:33Z",
|
|
420
|
+
* "updated_at": "2026-07-15T16:15:33Z"
|
|
421
|
+
* }
|
|
422
|
+
* ```
|
|
423
|
+
* @throws {ValidationError} If the file is empty, not a `.pdf`, exceeds
|
|
424
|
+
* 25 MB, or the API returns no document ID.
|
|
425
|
+
* @throws {ApiError} If the API rejects the upload.
|
|
338
426
|
*
|
|
339
427
|
* @example
|
|
340
428
|
* ```ts
|
|
341
429
|
* await client.documents.upload({ filePath: './contract.pdf' });
|
|
342
|
-
* await client.documents.upload(
|
|
430
|
+
* await client.documents.upload(
|
|
431
|
+
* { buffer, fileName: 'contract.pdf' },
|
|
432
|
+
* { name: 'Service agreement', metadata: { orderId: 'A-1' } },
|
|
433
|
+
* );
|
|
434
|
+
* // → name is stored as 'Service agreement.pdf'
|
|
343
435
|
* ```
|
|
344
436
|
*/
|
|
345
437
|
async upload(source, options = {}) {
|
|
346
|
-
const { buffer, fileName } = await loadSource(source);
|
|
347
|
-
validateUpload(buffer, fileName);
|
|
348
438
|
const accountId = this.accountId(options.accountId);
|
|
349
439
|
const formOptions = {};
|
|
440
|
+
if (options.name !== void 0) formOptions.name = options.name;
|
|
350
441
|
if (options.metadata !== void 0) formOptions.metadata = options.metadata;
|
|
351
|
-
const
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
442
|
+
const document = await this.uploadPdf(
|
|
443
|
+
`/accounts/${accountId}/documents`,
|
|
444
|
+
source,
|
|
445
|
+
formOptions,
|
|
446
|
+
{
|
|
447
|
+
errorLabel: "Document upload failed",
|
|
448
|
+
missingId: "Upload succeeded but no document ID was returned"
|
|
449
|
+
}
|
|
358
450
|
);
|
|
359
|
-
if (!document?.id) {
|
|
360
|
-
throw new ValidationError("Upload succeeded but no document ID was returned", {
|
|
361
|
-
response: document
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
451
|
this.logger.info("Document uploaded", { documentId: document.id });
|
|
365
452
|
return document;
|
|
366
453
|
}
|
|
367
454
|
/**
|
|
368
455
|
* List workspace documents. Pagination info (if any) is attached in `meta`.
|
|
369
|
-
* Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `
|
|
456
|
+
* Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per-page`.
|
|
370
457
|
*/
|
|
371
458
|
async list(params = {}, accountId) {
|
|
372
459
|
const id = this.accountId(accountId);
|
|
373
460
|
return this.callList(
|
|
374
461
|
"Failed to list documents",
|
|
375
|
-
() => this.http.get(`/accounts/${id}/documents`, { params:
|
|
462
|
+
() => this.http.get(`/accounts/${id}/documents`, { params: cleanListParams(params) })
|
|
463
|
+
);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Search workspace documents
|
|
467
|
+
* (`GET /accounts/{accountId}/documents/search`).
|
|
468
|
+
*
|
|
469
|
+
* A lighter-weight alternative to {@link DocumentResource.list}: it returns
|
|
470
|
+
* a compact representation with no expanded `assignment` or `pages`, so
|
|
471
|
+
* prefer it for name lookups and pickers.
|
|
472
|
+
*
|
|
473
|
+
* @param params - `search`, `status`, `page`, `per-page`.
|
|
474
|
+
* @param accountId - Override the client's default account ID.
|
|
475
|
+
* @returns Matching documents, with pagination in `meta`. Each item:
|
|
476
|
+
* ```jsonc
|
|
477
|
+
* {
|
|
478
|
+
* "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
479
|
+
* "account_id": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
|
|
480
|
+
* "template_id": null,
|
|
481
|
+
* "name": "Service agreement.pdf",
|
|
482
|
+
* "status": "pending_signature",
|
|
483
|
+
* "artifacts": { "original": "https://…" },
|
|
484
|
+
* "is_closed": false,
|
|
485
|
+
* "signing_url": "https://…",
|
|
486
|
+
* "decline_reason": null,
|
|
487
|
+
* "declined_by": null,
|
|
488
|
+
* "tags": [],
|
|
489
|
+
* "created_at": "2026-07-15T16:15:33Z",
|
|
490
|
+
* "updated_at": "2026-07-15T16:15:40Z"
|
|
491
|
+
* }
|
|
492
|
+
* ```
|
|
493
|
+
* @throws {ValidationError} If no account ID is available.
|
|
494
|
+
* @throws {ApiError} If the API rejects the request.
|
|
495
|
+
*
|
|
496
|
+
* @example
|
|
497
|
+
* ```ts
|
|
498
|
+
* const { data, meta } = await client.documents.search({
|
|
499
|
+
* search: 'agreement',
|
|
500
|
+
* status: 'pending_signature',
|
|
501
|
+
* 'per-page': 20,
|
|
502
|
+
* });
|
|
503
|
+
* ```
|
|
504
|
+
*/
|
|
505
|
+
async search(params = {}, accountId) {
|
|
506
|
+
const id = this.accountId(accountId);
|
|
507
|
+
return this.callList(
|
|
508
|
+
"Failed to search documents",
|
|
509
|
+
() => this.http.get(`/accounts/${id}/documents/search`, { params: cleanListParams(params) })
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Rename a document (`PATCH /documents/{documentId}`).
|
|
514
|
+
*
|
|
515
|
+
* Only valid while the document is still renameable: the API returns `400`
|
|
516
|
+
* ("Document cannot be renamed after the signature process has started")
|
|
517
|
+
* both once signing has begun **and** while the document is still in
|
|
518
|
+
* `metadata_processing` immediately after upload. Await
|
|
519
|
+
* {@link DocumentResource.waitUntilReady} before renaming a fresh upload.
|
|
520
|
+
*
|
|
521
|
+
* To set a name at upload time instead, pass `name` to
|
|
522
|
+
* {@link DocumentResource.upload} — that avoids the extra round-trip and
|
|
523
|
+
* the processing race entirely.
|
|
524
|
+
*
|
|
525
|
+
* @param documentId - The document to rename.
|
|
526
|
+
* @param name - The new display name (max 255 chars), e.g.
|
|
527
|
+
* `'Service agreement.pdf'`.
|
|
528
|
+
* @returns The updated document — **without** `pages` or `assignment`,
|
|
529
|
+
* which this endpoint does not return (unlike
|
|
530
|
+
* {@link DocumentResource.details}). Call `details()` if you need them.
|
|
531
|
+
* @throws {ValidationError} If `documentId` or `name` is missing.
|
|
532
|
+
* @throws {ApiError} `400` if the document is processing or already in
|
|
533
|
+
* signing; `404` if it does not exist.
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```ts
|
|
537
|
+
* const doc = await client.documents.upload({ filePath: './c.pdf' });
|
|
538
|
+
* await client.documents.waitUntilReady(doc.id); // else 400
|
|
539
|
+
* await client.documents.rename(doc.id, 'Service agreement.pdf');
|
|
540
|
+
* ```
|
|
541
|
+
*/
|
|
542
|
+
async rename(documentId, name) {
|
|
543
|
+
const id = this.requireId(documentId, "Document ID");
|
|
544
|
+
const newName = this.requireId(name, "Name");
|
|
545
|
+
this.logger.info("Renaming document", { documentId: id });
|
|
546
|
+
return this.call(
|
|
547
|
+
"Failed to rename document",
|
|
548
|
+
() => this.http.patch(`/documents/${id}`, { name: newName })
|
|
376
549
|
);
|
|
377
550
|
}
|
|
378
551
|
/** Get document details. */
|
|
@@ -406,6 +579,9 @@ var DocumentResource = class extends BaseResource {
|
|
|
406
579
|
}
|
|
407
580
|
} catch (err) {
|
|
408
581
|
if (err instanceof ValidationError) throw err;
|
|
582
|
+
if (err instanceof ApiError && err.statusCode < 500 && err.statusCode !== 429) {
|
|
583
|
+
throw err;
|
|
584
|
+
}
|
|
409
585
|
this.logger.warn("Error checking document status", {
|
|
410
586
|
error: err instanceof Error ? err.message : String(err)
|
|
411
587
|
});
|
|
@@ -636,7 +812,8 @@ var SignerResource = class extends BaseResource {
|
|
|
636
812
|
() => this.http.post(`/accounts/${id}/signers`, normaliseSignerPayload(payload))
|
|
637
813
|
);
|
|
638
814
|
} catch (err) {
|
|
639
|
-
|
|
815
|
+
const isDuplicateStatus = err instanceof ApiError && (err.statusCode === 409 || err.statusCode === 400);
|
|
816
|
+
if (isDuplicateStatus && payload.email) {
|
|
640
817
|
const duplicate = await this.findByEmail(payload.email, id);
|
|
641
818
|
if (duplicate) {
|
|
642
819
|
this.logger.info("Signer already exists, using existing signer", {
|
|
@@ -657,12 +834,12 @@ var SignerResource = class extends BaseResource {
|
|
|
657
834
|
() => this.http.get(`/accounts/${id}/signers/${sid}`)
|
|
658
835
|
);
|
|
659
836
|
}
|
|
660
|
-
/** List signers for the workspace (supports `page`, `
|
|
837
|
+
/** List signers for the workspace (supports `page`, `per-page`, `search`, `sort`). */
|
|
661
838
|
async list(params = {}, accountId) {
|
|
662
839
|
const id = this.accountId(accountId);
|
|
663
840
|
return this.callList(
|
|
664
841
|
"Failed to list signers",
|
|
665
|
-
() => this.http.get(`/accounts/${id}/signers`, { params:
|
|
842
|
+
() => this.http.get(`/accounts/${id}/signers`, { params: cleanListParams(params) })
|
|
666
843
|
);
|
|
667
844
|
}
|
|
668
845
|
/** Update a signer. Fails if the signer has active assignments. */
|
|
@@ -683,11 +860,28 @@ var SignerResource = class extends BaseResource {
|
|
|
683
860
|
() => this.http.delete(`/accounts/${id}/signers/${sid}`)
|
|
684
861
|
);
|
|
685
862
|
}
|
|
686
|
-
/**
|
|
863
|
+
/**
|
|
864
|
+
* Find a signer by exact email, using the API's `search` filter to narrow
|
|
865
|
+
* the page first. Returns `null` if none match.
|
|
866
|
+
*
|
|
867
|
+
* `search` is a substring match across signer fields, so the result is
|
|
868
|
+
* re-filtered here for an exact, case-insensitive email match.
|
|
869
|
+
*
|
|
870
|
+
* Page size is pinned to the API's maximum of 50: larger values are
|
|
871
|
+
* silently clamped to 50 by the server, so asking for more is misleading.
|
|
872
|
+
* An exact address realistically matches one signer, but a search term that
|
|
873
|
+
* matched more than 50 could in principle miss one — the API exposes no
|
|
874
|
+
* exact-email filter to rule that out.
|
|
875
|
+
*
|
|
876
|
+
* @param email - Exact email address to look for.
|
|
877
|
+
* @param accountId - Override the client's default account ID.
|
|
878
|
+
* @returns The matching {@link ISigner}, or `null`.
|
|
879
|
+
* @throws {ValidationError} If `email` is not a valid address.
|
|
880
|
+
*/
|
|
687
881
|
async findByEmail(email, accountId) {
|
|
688
882
|
this.assertEmail(email);
|
|
689
883
|
try {
|
|
690
|
-
const { data } = await this.list({ search: email,
|
|
884
|
+
const { data } = await this.list({ search: email, "per-page": 50 }, accountId);
|
|
691
885
|
const lower = email.toLowerCase();
|
|
692
886
|
return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
|
|
693
887
|
} catch (err) {
|
|
@@ -799,6 +993,58 @@ function normaliseSignerRef(ref, options) {
|
|
|
799
993
|
throw new ValidationError("Invalid signer reference", { ref });
|
|
800
994
|
}
|
|
801
995
|
var AssignmentResource = class extends BaseResource {
|
|
996
|
+
/**
|
|
997
|
+
* List assignments across the workspace (`GET /assignments`).
|
|
998
|
+
*
|
|
999
|
+
* The account is passed as an `accountId` **query parameter** — the API
|
|
1000
|
+
* responds `400` ("Um contexto de conta é necessário e não foi fornecido")
|
|
1001
|
+
* without it. Note the camelCase spelling: `account_id` and an
|
|
1002
|
+
* `X-Account-Id` header are both rejected.
|
|
1003
|
+
*
|
|
1004
|
+
* @param params - `page`, `per-page`.
|
|
1005
|
+
* @param accountId - Override the client's default account ID.
|
|
1006
|
+
* @returns Assignments, with pagination in `meta`. Each item:
|
|
1007
|
+
* ```jsonc
|
|
1008
|
+
* {
|
|
1009
|
+
* "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4",
|
|
1010
|
+
* "sender_email": "sender@example.com",
|
|
1011
|
+
* "method": "virtual",
|
|
1012
|
+
* "expires_at": null,
|
|
1013
|
+
* "message": "Please sign this contract",
|
|
1014
|
+
* "signers": [
|
|
1015
|
+
* {
|
|
1016
|
+
* "id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
|
|
1017
|
+
* "full_name": "Ana Souza",
|
|
1018
|
+
* "email": "signer@example.com",
|
|
1019
|
+
* "whatsapp_phone_number": null,
|
|
1020
|
+
* "has_accepted_terms": false,
|
|
1021
|
+
* "completed": false,
|
|
1022
|
+
* "notification_history": [],
|
|
1023
|
+
* "verification_method": "Email",
|
|
1024
|
+
* "notification_methods": ["Email"],
|
|
1025
|
+
* "step": 1,
|
|
1026
|
+
* "notified": true
|
|
1027
|
+
* }
|
|
1028
|
+
* ]
|
|
1029
|
+
* }
|
|
1030
|
+
* ```
|
|
1031
|
+
* @throws {ValidationError} If no account ID is available.
|
|
1032
|
+
* @throws {ApiError} If the API rejects the request.
|
|
1033
|
+
*
|
|
1034
|
+
* @example
|
|
1035
|
+
* ```ts
|
|
1036
|
+
* const { data, meta } = await client.assignments.list({ 'per-page': 20 });
|
|
1037
|
+
* ```
|
|
1038
|
+
*/
|
|
1039
|
+
async list(params = {}, accountId) {
|
|
1040
|
+
const id = this.accountId(accountId);
|
|
1041
|
+
return this.callList(
|
|
1042
|
+
"Failed to list assignments",
|
|
1043
|
+
() => this.http.get("/assignments", {
|
|
1044
|
+
params: { accountId: id, ...cleanListParams(params) }
|
|
1045
|
+
})
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
802
1048
|
/** Create a signing assignment for a document. */
|
|
803
1049
|
async create(documentId, payload) {
|
|
804
1050
|
const docId = this.requireId(documentId, "Document ID");
|
|
@@ -934,16 +1180,14 @@ var WebhookResource = class extends BaseResource {
|
|
|
934
1180
|
() => this.http.get(`/accounts/${id}/webhooks/subscriptions`)
|
|
935
1181
|
);
|
|
936
1182
|
}
|
|
937
|
-
/**
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
/** Inactivate the current webhook subscription without deleting it. */
|
|
1183
|
+
/**
|
|
1184
|
+
* Inactivate the current webhook subscription.
|
|
1185
|
+
*
|
|
1186
|
+
* This is the only supported way to stop deliveries — the API has no
|
|
1187
|
+
* subscription-delete route. The subscription is retained (with its `url`
|
|
1188
|
+
* and `events`) and simply stops firing; re-enable it by calling
|
|
1189
|
+
* {@link WebhookResource.register} again with `is_active: true`.
|
|
1190
|
+
*/
|
|
947
1191
|
async inactivate(accountId) {
|
|
948
1192
|
const id = this.accountId(accountId);
|
|
949
1193
|
this.logger.info("Inactivating webhook subscription");
|
|
@@ -965,7 +1209,7 @@ var WebhookResource = class extends BaseResource {
|
|
|
965
1209
|
return this.callList(
|
|
966
1210
|
"Failed to list webhook dispatches",
|
|
967
1211
|
() => this.http.get(`/accounts/${id}/webhooks`, {
|
|
968
|
-
params:
|
|
1212
|
+
params: cleanListParams(params)
|
|
969
1213
|
})
|
|
970
1214
|
);
|
|
971
1215
|
}
|
|
@@ -1001,24 +1245,18 @@ var TemplateResource = class extends BaseResource {
|
|
|
1001
1245
|
* ```
|
|
1002
1246
|
*/
|
|
1003
1247
|
async create(source, options = {}) {
|
|
1004
|
-
const { buffer, fileName } = await loadSource(source);
|
|
1005
|
-
validateUpload(buffer, fileName);
|
|
1006
1248
|
const id = this.accountId(options.accountId);
|
|
1007
1249
|
const formOptions = {};
|
|
1008
1250
|
if (options.name !== void 0) formOptions.name = options.name;
|
|
1009
|
-
const
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1251
|
+
const template = await this.uploadPdf(
|
|
1252
|
+
`/accounts/${id}/templates`,
|
|
1253
|
+
source,
|
|
1254
|
+
formOptions,
|
|
1255
|
+
{
|
|
1256
|
+
errorLabel: "Failed to create template",
|
|
1257
|
+
missingId: "Template upload succeeded but no template ID was returned"
|
|
1258
|
+
}
|
|
1016
1259
|
);
|
|
1017
|
-
if (!template?.id) {
|
|
1018
|
-
throw new ValidationError("Template upload succeeded but no template ID was returned", {
|
|
1019
|
-
response: template
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
1260
|
this.logger.info("Template created", { templateId: template.id });
|
|
1023
1261
|
return template;
|
|
1024
1262
|
}
|
|
@@ -1027,14 +1265,17 @@ var TemplateResource = class extends BaseResource {
|
|
|
1027
1265
|
const id = this.accountId(accountId);
|
|
1028
1266
|
return this.callList(
|
|
1029
1267
|
"Failed to list templates",
|
|
1030
|
-
() => this.http.get(`/accounts/${id}/templates`, { params:
|
|
1268
|
+
() => this.http.get(`/accounts/${id}/templates`, { params: cleanListParams(params) })
|
|
1031
1269
|
);
|
|
1032
1270
|
}
|
|
1033
1271
|
/**
|
|
1034
1272
|
* Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
|
|
1035
1273
|
*
|
|
1036
|
-
*
|
|
1037
|
-
* (
|
|
1274
|
+
* Returns the same shape as {@link TemplateResource.list} plus
|
|
1275
|
+
* `default_document_tags` (the tags auto-applied to every document created
|
|
1276
|
+
* from this template) and `resource`. Both endpoints return `pages` with
|
|
1277
|
+
* per-page `download_url`, so fetching a template again purely to read its
|
|
1278
|
+
* pages is unnecessary.
|
|
1038
1279
|
*/
|
|
1039
1280
|
async get(templateId, accountId) {
|
|
1040
1281
|
const id = this.accountId(accountId);
|
|
@@ -1102,7 +1343,7 @@ var TagResource = class extends BaseResource {
|
|
|
1102
1343
|
return this.call(
|
|
1103
1344
|
"Failed to list tags",
|
|
1104
1345
|
() => this.http.get(`/accounts/${id}/tags`, {
|
|
1105
|
-
params:
|
|
1346
|
+
params: cleanListParams(params)
|
|
1106
1347
|
})
|
|
1107
1348
|
);
|
|
1108
1349
|
}
|
|
@@ -1244,7 +1485,7 @@ var FieldsResource = class extends BaseResource {
|
|
|
1244
1485
|
return this.call(
|
|
1245
1486
|
"Failed to list field definitions",
|
|
1246
1487
|
() => this.http.get(`/accounts/${id}/fields`, {
|
|
1247
|
-
params:
|
|
1488
|
+
params: cleanListParams(params)
|
|
1248
1489
|
})
|
|
1249
1490
|
);
|
|
1250
1491
|
}
|
|
@@ -1328,7 +1569,44 @@ var SignerDocumentsResource = class extends BaseResource {
|
|
|
1328
1569
|
return this.callList(
|
|
1329
1570
|
"Failed to list signer documents",
|
|
1330
1571
|
() => this.http.get(`/signers/${sid}/documents`, {
|
|
1331
|
-
params: { "signer-access-code": code, ...
|
|
1572
|
+
params: { "signer-access-code": code, ...cleanListParams(params) }
|
|
1573
|
+
})
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Search the documents awaiting a given signer
|
|
1578
|
+
* (`GET /signers/{signer_id}/documents/search?signer-access-code=…`).
|
|
1579
|
+
*
|
|
1580
|
+
* The signer-side counterpart of {@link DocumentResource.search}, scoped to
|
|
1581
|
+
* one signer and authorised by their access code rather than the API key.
|
|
1582
|
+
* Like {@link SignerDocumentsResource.list}, it requires
|
|
1583
|
+
* `signer-access-code`; the published spec omits that parameter, but the
|
|
1584
|
+
* endpoint is not usable without it.
|
|
1585
|
+
*
|
|
1586
|
+
* @param signerId - The signer whose documents are searched.
|
|
1587
|
+
* @param signerAccessCode - The signer's access code, from their signing link.
|
|
1588
|
+
* @param search - Free-text term matched against the document name.
|
|
1589
|
+
* @returns Matching documents for that signer, in the compact
|
|
1590
|
+
* {@link IDocumentListItem} shape, with pagination in `meta`.
|
|
1591
|
+
* @throws {ValidationError} If `signerId` or `signerAccessCode` is missing.
|
|
1592
|
+
* @throws {ApiError} If the access code is invalid or expired.
|
|
1593
|
+
*
|
|
1594
|
+
* @example
|
|
1595
|
+
* ```ts
|
|
1596
|
+
* const { data } = await client.signerDocuments.search(
|
|
1597
|
+
* signerId,
|
|
1598
|
+
* accessCode,
|
|
1599
|
+
* 'agreement',
|
|
1600
|
+
* );
|
|
1601
|
+
* ```
|
|
1602
|
+
*/
|
|
1603
|
+
async search(signerId, signerAccessCode, search) {
|
|
1604
|
+
const sid = this.requireId(signerId, "Signer ID");
|
|
1605
|
+
const code = this.requireId(signerAccessCode, "signer-access-code");
|
|
1606
|
+
return this.callList(
|
|
1607
|
+
"Failed to search signer documents",
|
|
1608
|
+
() => this.http.get(`/signers/${sid}/documents/search`, {
|
|
1609
|
+
params: cleanParams({ "signer-access-code": code, search })
|
|
1332
1610
|
})
|
|
1333
1611
|
);
|
|
1334
1612
|
}
|
|
@@ -1548,6 +1826,21 @@ var WebhookVerifier = class {
|
|
|
1548
1826
|
// src/client.ts
|
|
1549
1827
|
var DEFAULT_BASE_URL = "https://api.assinafy.com.br/v1";
|
|
1550
1828
|
var AssinafyClient = class _AssinafyClient {
|
|
1829
|
+
axiosInstance;
|
|
1830
|
+
defaultAccountId;
|
|
1831
|
+
logger;
|
|
1832
|
+
webhookSecret;
|
|
1833
|
+
documents;
|
|
1834
|
+
signers;
|
|
1835
|
+
workspaces;
|
|
1836
|
+
assignments;
|
|
1837
|
+
webhooks;
|
|
1838
|
+
templates;
|
|
1839
|
+
tags;
|
|
1840
|
+
auth;
|
|
1841
|
+
fields;
|
|
1842
|
+
signerDocuments;
|
|
1843
|
+
webhookVerifier;
|
|
1551
1844
|
constructor(options) {
|
|
1552
1845
|
if (!options.apiKey && !options.token) {
|
|
1553
1846
|
throw new ValidationError(
|
|
@@ -1706,6 +1999,7 @@ function installRateLimitRetry(http, maxRetries, logger) {
|
|
|
1706
1999
|
DEFAULT_WEBHOOK_EVENTS,
|
|
1707
2000
|
DocumentResource,
|
|
1708
2001
|
FieldsResource,
|
|
2002
|
+
MAX_UPLOAD_BYTES,
|
|
1709
2003
|
NetworkError,
|
|
1710
2004
|
SignerDocumentsResource,
|
|
1711
2005
|
SignerResource,
|