@assinafy/sdk 1.4.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 +105 -19
- package/dist/index.d.mts +572 -74
- package/dist/index.d.ts +572 -74
- package/dist/index.js +564 -115
- package/dist/index.mjs +562 -115
- package/package.json +24 -12
package/dist/index.mjs
CHANGED
|
@@ -3,16 +3,16 @@ import axios2 from "axios";
|
|
|
3
3
|
|
|
4
4
|
// src/errors.ts
|
|
5
5
|
var AssinafyError = class extends Error {
|
|
6
|
+
context;
|
|
6
7
|
constructor(message, context = {}, options) {
|
|
7
|
-
super(message);
|
|
8
|
+
super(message, options);
|
|
8
9
|
this.name = "AssinafyError";
|
|
9
10
|
this.context = context;
|
|
10
|
-
if (options?.cause !== void 0) {
|
|
11
|
-
this.cause = options.cause;
|
|
12
|
-
}
|
|
13
11
|
}
|
|
14
12
|
};
|
|
15
13
|
var ApiError = class _ApiError extends AssinafyError {
|
|
14
|
+
statusCode;
|
|
15
|
+
responseData;
|
|
16
16
|
constructor(message, statusCode, responseData = null, options) {
|
|
17
17
|
super(message, { statusCode, responseData }, options);
|
|
18
18
|
this.name = "ApiError";
|
|
@@ -28,6 +28,7 @@ var ApiError = class _ApiError extends AssinafyError {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
var ValidationError = class extends AssinafyError {
|
|
31
|
+
errors;
|
|
31
32
|
constructor(message = "Validation failed", errors = {}) {
|
|
32
33
|
super(message, { errors });
|
|
33
34
|
this.name = "ValidationError";
|
|
@@ -53,6 +54,23 @@ function handleAssinafyResponse(response) {
|
|
|
53
54
|
}
|
|
54
55
|
return response;
|
|
55
56
|
}
|
|
57
|
+
function decodeBinaryErrorBody(data) {
|
|
58
|
+
let text;
|
|
59
|
+
if (typeof Buffer !== "undefined" && Buffer.isBuffer(data)) {
|
|
60
|
+
text = data.toString("utf8");
|
|
61
|
+
} else if (data instanceof ArrayBuffer) {
|
|
62
|
+
text = Buffer.from(data).toString("utf8");
|
|
63
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
64
|
+
text = Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8");
|
|
65
|
+
} else {
|
|
66
|
+
return data;
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(text);
|
|
70
|
+
} catch {
|
|
71
|
+
return text.length > 0 ? { message: text } : null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
56
74
|
function toSdkError(error, fallbackMessage) {
|
|
57
75
|
if (error instanceof AssinafyError) {
|
|
58
76
|
return error;
|
|
@@ -60,14 +78,15 @@ function toSdkError(error, fallbackMessage) {
|
|
|
60
78
|
if (axios.isAxiosError(error)) {
|
|
61
79
|
const status = error.response?.status;
|
|
62
80
|
if (status) {
|
|
63
|
-
|
|
81
|
+
const body = decodeBinaryErrorBody(error.response?.data ?? null);
|
|
82
|
+
return ApiError.fromResponse(status, body ?? null);
|
|
64
83
|
}
|
|
65
84
|
return new NetworkError(`${fallbackMessage}: ${error.message}`, { cause: error });
|
|
66
85
|
}
|
|
67
86
|
if (error instanceof Error) {
|
|
68
87
|
return new AssinafyError(`${fallbackMessage}: ${error.message}`, {}, { cause: error });
|
|
69
88
|
}
|
|
70
|
-
return new AssinafyError(fallbackMessage, { cause: error });
|
|
89
|
+
return new AssinafyError(fallbackMessage, {}, { cause: error });
|
|
71
90
|
}
|
|
72
91
|
function createNoopLogger() {
|
|
73
92
|
return {
|
|
@@ -86,12 +105,101 @@ function cleanParams(params) {
|
|
|
86
105
|
}
|
|
87
106
|
return out;
|
|
88
107
|
}
|
|
108
|
+
function cleanListParams(params) {
|
|
109
|
+
const out = cleanParams(params);
|
|
110
|
+
if (out["per_page"] !== void 0) {
|
|
111
|
+
out["per-page"] ??= out["per_page"];
|
|
112
|
+
delete out["per_page"];
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
89
116
|
|
|
90
|
-
// src/
|
|
117
|
+
// src/support/retry.ts
|
|
118
|
+
function header(headers, name) {
|
|
119
|
+
if (!headers) return void 0;
|
|
120
|
+
const lower = name.toLowerCase();
|
|
121
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
122
|
+
if (key.toLowerCase() === lower && value != null) {
|
|
123
|
+
return Array.isArray(value) ? String(value[0]) : String(value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
function retryDelayFromHeaders(headers) {
|
|
129
|
+
const retryAfter = header(headers, "retry-after");
|
|
130
|
+
if (retryAfter !== void 0) {
|
|
131
|
+
const seconds = Number(retryAfter);
|
|
132
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
133
|
+
const date = Date.parse(retryAfter);
|
|
134
|
+
if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
|
|
135
|
+
}
|
|
136
|
+
const reset = header(headers, "x-rate-limit-reset");
|
|
137
|
+
if (reset !== void 0) {
|
|
138
|
+
const seconds = Number(reset);
|
|
139
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
|
|
140
|
+
}
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
function backoffMs(attempt) {
|
|
144
|
+
return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 8e3);
|
|
145
|
+
}
|
|
146
|
+
function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
|
|
147
|
+
const hinted = retryDelayFromHeaders(headers);
|
|
148
|
+
if (hinted !== void 0) return Math.min(hinted, maxDelayMs);
|
|
149
|
+
return Math.min(backoffMs(attempt), maxDelayMs);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/resources/upload.ts
|
|
91
153
|
import { promises as fs } from "fs";
|
|
92
154
|
import path from "path";
|
|
155
|
+
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
156
|
+
async function loadSource(source) {
|
|
157
|
+
if ("buffer" in source) {
|
|
158
|
+
if (!source.fileName) {
|
|
159
|
+
throw new ValidationError("fileName is required when uploading a Buffer");
|
|
160
|
+
}
|
|
161
|
+
return { buffer: source.buffer, fileName: source.fileName };
|
|
162
|
+
}
|
|
163
|
+
if (!source.filePath) {
|
|
164
|
+
throw new ValidationError("filePath is required");
|
|
165
|
+
}
|
|
166
|
+
const buffer = await fs.readFile(source.filePath);
|
|
167
|
+
return { buffer, fileName: source.fileName ?? path.basename(source.filePath) };
|
|
168
|
+
}
|
|
169
|
+
function validateUpload(buffer, fileName) {
|
|
170
|
+
if (!buffer || buffer.byteLength === 0) {
|
|
171
|
+
throw new ValidationError("File buffer is empty", { fileName });
|
|
172
|
+
}
|
|
173
|
+
if (!fileName.toLowerCase().endsWith(".pdf")) {
|
|
174
|
+
throw new ValidationError("Only PDF files are supported", { fileName });
|
|
175
|
+
}
|
|
176
|
+
if (buffer.byteLength > MAX_UPLOAD_BYTES) {
|
|
177
|
+
throw new ValidationError("File size exceeds maximum allowed (25MB)", {
|
|
178
|
+
fileSize: buffer.byteLength,
|
|
179
|
+
maxSize: MAX_UPLOAD_BYTES
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function toUploadFileName(name) {
|
|
184
|
+
return name.toLowerCase().endsWith(".pdf") ? name : `${name}.pdf`;
|
|
185
|
+
}
|
|
186
|
+
function buildUploadForm(buffer, fileName, options = {}) {
|
|
187
|
+
const form = new FormData();
|
|
188
|
+
const view = new Uint8Array(
|
|
189
|
+
buffer.buffer,
|
|
190
|
+
buffer.byteOffset,
|
|
191
|
+
buffer.byteLength
|
|
192
|
+
);
|
|
193
|
+
const partName = options.name === void 0 ? fileName : toUploadFileName(options.name);
|
|
194
|
+
form.append("file", new Blob([view], { type: "application/pdf" }), partName);
|
|
195
|
+
if (options.metadata) {
|
|
196
|
+
form.append("metadata", JSON.stringify(options.metadata));
|
|
197
|
+
}
|
|
198
|
+
return form;
|
|
199
|
+
}
|
|
93
200
|
|
|
94
201
|
// src/resources/base.ts
|
|
202
|
+
var MULTIPART_CONTENT_TYPE = "multipart/form-data";
|
|
95
203
|
var BaseResource = class {
|
|
96
204
|
constructor(http, defaultAccountId, logger = createNoopLogger()) {
|
|
97
205
|
this.http = http;
|
|
@@ -153,6 +261,35 @@ var BaseResource = class {
|
|
|
153
261
|
throw toSdkError(err, label);
|
|
154
262
|
}
|
|
155
263
|
}
|
|
264
|
+
/**
|
|
265
|
+
* Upload a PDF as `multipart/form-data` and assert the API echoed an id.
|
|
266
|
+
*
|
|
267
|
+
* Shared by `documents.upload` and `templates.create`, which are the same
|
|
268
|
+
* sequence over different paths: load → validate → build form → POST →
|
|
269
|
+
* assert an id came back. Callers keep their own success logging.
|
|
270
|
+
*
|
|
271
|
+
* @param path - Account-scoped endpoint to POST to.
|
|
272
|
+
* @param source - The PDF, as a file path or in-memory buffer.
|
|
273
|
+
* @param formOptions - `name` (display name) and optional `metadata`.
|
|
274
|
+
* @param labels - `errorLabel` for the request failure, `missingId` for a
|
|
275
|
+
* `2xx` that returned no id.
|
|
276
|
+
*/
|
|
277
|
+
async uploadPdf(path2, source, formOptions, labels) {
|
|
278
|
+
const { buffer, fileName } = await loadSource(source);
|
|
279
|
+
validateUpload(buffer, fileName);
|
|
280
|
+
this.logger.info("Uploading PDF", { path: path2, fileName, size: buffer.byteLength });
|
|
281
|
+
const form = buildUploadForm(buffer, fileName, formOptions);
|
|
282
|
+
const result = await this.call(
|
|
283
|
+
labels.errorLabel,
|
|
284
|
+
() => this.http.post(path2, form, { headers: { "Content-Type": MULTIPART_CONTENT_TYPE } })
|
|
285
|
+
);
|
|
286
|
+
if (!result?.id) {
|
|
287
|
+
throw new ValidationError(labels.missingId, {
|
|
288
|
+
response: result
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
156
293
|
/** Execute a paginated list call and attach meta from `X-Pagination-*` headers. */
|
|
157
294
|
async callList(label, request) {
|
|
158
295
|
try {
|
|
@@ -195,7 +332,6 @@ function toInt(value) {
|
|
|
195
332
|
}
|
|
196
333
|
|
|
197
334
|
// src/resources/documents.ts
|
|
198
|
-
var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
199
335
|
var READY_STATUSES = /* @__PURE__ */ new Set([
|
|
200
336
|
"metadata_ready",
|
|
201
337
|
"pending_signature",
|
|
@@ -209,43 +345,153 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
|
|
|
209
345
|
]);
|
|
210
346
|
var DocumentResource = class extends BaseResource {
|
|
211
347
|
/**
|
|
212
|
-
* Upload a PDF to the workspace.
|
|
348
|
+
* Upload a PDF to the workspace (`POST /accounts/{accountId}/documents`).
|
|
349
|
+
*
|
|
350
|
+
* The document is created in `metadata_processing` status and becomes
|
|
351
|
+
* usable once it reaches `metadata_ready`; use
|
|
352
|
+
* {@link DocumentResource.waitUntilReady} to await that transition. Note
|
|
353
|
+
* that {@link DocumentResource.rename} and {@link DocumentResource.delete}
|
|
354
|
+
* return `400` while the document is still processing.
|
|
355
|
+
*
|
|
356
|
+
* @param source - The PDF to upload, as a file path or an in-memory buffer.
|
|
357
|
+
* @param options - Display name, metadata, and account override.
|
|
358
|
+
* @returns The created document. Response shape:
|
|
359
|
+
* ```jsonc
|
|
360
|
+
* {
|
|
361
|
+
* "resource": "document",
|
|
362
|
+
* "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
363
|
+
* "name": "Service agreement.pdf",
|
|
364
|
+
* "status": "metadata_processing",
|
|
365
|
+
* "created_at": "2026-07-15T16:15:33Z",
|
|
366
|
+
* "updated_at": "2026-07-15T16:15:33Z"
|
|
367
|
+
* }
|
|
368
|
+
* ```
|
|
369
|
+
* @throws {ValidationError} If the file is empty, not a `.pdf`, exceeds
|
|
370
|
+
* 25 MB, or the API returns no document ID.
|
|
371
|
+
* @throws {ApiError} If the API rejects the upload.
|
|
213
372
|
*
|
|
214
373
|
* @example
|
|
215
374
|
* ```ts
|
|
216
375
|
* await client.documents.upload({ filePath: './contract.pdf' });
|
|
217
|
-
* await client.documents.upload(
|
|
376
|
+
* await client.documents.upload(
|
|
377
|
+
* { buffer, fileName: 'contract.pdf' },
|
|
378
|
+
* { name: 'Service agreement', metadata: { orderId: 'A-1' } },
|
|
379
|
+
* );
|
|
380
|
+
* // → name is stored as 'Service agreement.pdf'
|
|
218
381
|
* ```
|
|
219
382
|
*/
|
|
220
383
|
async upload(source, options = {}) {
|
|
221
|
-
const { buffer, fileName } = await loadSource(source);
|
|
222
|
-
validateUpload(buffer, fileName);
|
|
223
384
|
const accountId = this.accountId(options.accountId);
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
385
|
+
const formOptions = {};
|
|
386
|
+
if (options.name !== void 0) formOptions.name = options.name;
|
|
387
|
+
if (options.metadata !== void 0) formOptions.metadata = options.metadata;
|
|
388
|
+
const document = await this.uploadPdf(
|
|
389
|
+
`/accounts/${accountId}/documents`,
|
|
390
|
+
source,
|
|
391
|
+
formOptions,
|
|
392
|
+
{
|
|
393
|
+
errorLabel: "Document upload failed",
|
|
394
|
+
missingId: "Upload succeeded but no document ID was returned"
|
|
395
|
+
}
|
|
231
396
|
);
|
|
232
|
-
if (!document?.id) {
|
|
233
|
-
throw new ValidationError("Upload succeeded but no document ID was returned", {
|
|
234
|
-
response: document
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
397
|
this.logger.info("Document uploaded", { documentId: document.id });
|
|
238
398
|
return document;
|
|
239
399
|
}
|
|
240
400
|
/**
|
|
241
401
|
* List workspace documents. Pagination info (if any) is attached in `meta`.
|
|
242
|
-
* Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `
|
|
402
|
+
* Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per-page`.
|
|
243
403
|
*/
|
|
244
404
|
async list(params = {}, accountId) {
|
|
245
405
|
const id = this.accountId(accountId);
|
|
246
406
|
return this.callList(
|
|
247
407
|
"Failed to list documents",
|
|
248
|
-
() => this.http.get(`/accounts/${id}/documents`, { params:
|
|
408
|
+
() => this.http.get(`/accounts/${id}/documents`, { params: cleanListParams(params) })
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Search workspace documents
|
|
413
|
+
* (`GET /accounts/{accountId}/documents/search`).
|
|
414
|
+
*
|
|
415
|
+
* A lighter-weight alternative to {@link DocumentResource.list}: it returns
|
|
416
|
+
* a compact representation with no expanded `assignment` or `pages`, so
|
|
417
|
+
* prefer it for name lookups and pickers.
|
|
418
|
+
*
|
|
419
|
+
* @param params - `search`, `status`, `page`, `per-page`.
|
|
420
|
+
* @param accountId - Override the client's default account ID.
|
|
421
|
+
* @returns Matching documents, with pagination in `meta`. Each item:
|
|
422
|
+
* ```jsonc
|
|
423
|
+
* {
|
|
424
|
+
* "id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
|
|
425
|
+
* "account_id": "d4e5f6a7b8c9d0e1f2a3b4c5d6e7",
|
|
426
|
+
* "template_id": null,
|
|
427
|
+
* "name": "Service agreement.pdf",
|
|
428
|
+
* "status": "pending_signature",
|
|
429
|
+
* "artifacts": { "original": "https://…" },
|
|
430
|
+
* "is_closed": false,
|
|
431
|
+
* "signing_url": "https://…",
|
|
432
|
+
* "decline_reason": null,
|
|
433
|
+
* "declined_by": null,
|
|
434
|
+
* "tags": [],
|
|
435
|
+
* "created_at": "2026-07-15T16:15:33Z",
|
|
436
|
+
* "updated_at": "2026-07-15T16:15:40Z"
|
|
437
|
+
* }
|
|
438
|
+
* ```
|
|
439
|
+
* @throws {ValidationError} If no account ID is available.
|
|
440
|
+
* @throws {ApiError} If the API rejects the request.
|
|
441
|
+
*
|
|
442
|
+
* @example
|
|
443
|
+
* ```ts
|
|
444
|
+
* const { data, meta } = await client.documents.search({
|
|
445
|
+
* search: 'agreement',
|
|
446
|
+
* status: 'pending_signature',
|
|
447
|
+
* 'per-page': 20,
|
|
448
|
+
* });
|
|
449
|
+
* ```
|
|
450
|
+
*/
|
|
451
|
+
async search(params = {}, accountId) {
|
|
452
|
+
const id = this.accountId(accountId);
|
|
453
|
+
return this.callList(
|
|
454
|
+
"Failed to search documents",
|
|
455
|
+
() => this.http.get(`/accounts/${id}/documents/search`, { params: cleanListParams(params) })
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Rename a document (`PATCH /documents/{documentId}`).
|
|
460
|
+
*
|
|
461
|
+
* Only valid while the document is still renameable: the API returns `400`
|
|
462
|
+
* ("Document cannot be renamed after the signature process has started")
|
|
463
|
+
* both once signing has begun **and** while the document is still in
|
|
464
|
+
* `metadata_processing` immediately after upload. Await
|
|
465
|
+
* {@link DocumentResource.waitUntilReady} before renaming a fresh upload.
|
|
466
|
+
*
|
|
467
|
+
* To set a name at upload time instead, pass `name` to
|
|
468
|
+
* {@link DocumentResource.upload} — that avoids the extra round-trip and
|
|
469
|
+
* the processing race entirely.
|
|
470
|
+
*
|
|
471
|
+
* @param documentId - The document to rename.
|
|
472
|
+
* @param name - The new display name (max 255 chars), e.g.
|
|
473
|
+
* `'Service agreement.pdf'`.
|
|
474
|
+
* @returns The updated document — **without** `pages` or `assignment`,
|
|
475
|
+
* which this endpoint does not return (unlike
|
|
476
|
+
* {@link DocumentResource.details}). Call `details()` if you need them.
|
|
477
|
+
* @throws {ValidationError} If `documentId` or `name` is missing.
|
|
478
|
+
* @throws {ApiError} `400` if the document is processing or already in
|
|
479
|
+
* signing; `404` if it does not exist.
|
|
480
|
+
*
|
|
481
|
+
* @example
|
|
482
|
+
* ```ts
|
|
483
|
+
* const doc = await client.documents.upload({ filePath: './c.pdf' });
|
|
484
|
+
* await client.documents.waitUntilReady(doc.id); // else 400
|
|
485
|
+
* await client.documents.rename(doc.id, 'Service agreement.pdf');
|
|
486
|
+
* ```
|
|
487
|
+
*/
|
|
488
|
+
async rename(documentId, name) {
|
|
489
|
+
const id = this.requireId(documentId, "Document ID");
|
|
490
|
+
const newName = this.requireId(name, "Name");
|
|
491
|
+
this.logger.info("Renaming document", { documentId: id });
|
|
492
|
+
return this.call(
|
|
493
|
+
"Failed to rename document",
|
|
494
|
+
() => this.http.patch(`/documents/${id}`, { name: newName })
|
|
249
495
|
);
|
|
250
496
|
}
|
|
251
497
|
/** Get document details. */
|
|
@@ -279,6 +525,9 @@ var DocumentResource = class extends BaseResource {
|
|
|
279
525
|
}
|
|
280
526
|
} catch (err) {
|
|
281
527
|
if (err instanceof ValidationError) throw err;
|
|
528
|
+
if (err instanceof ApiError && err.statusCode < 500 && err.statusCode !== 429) {
|
|
529
|
+
throw err;
|
|
530
|
+
}
|
|
282
531
|
this.logger.warn("Error checking document status", {
|
|
283
532
|
error: err instanceof Error ? err.message : String(err)
|
|
284
533
|
});
|
|
@@ -397,7 +646,12 @@ var DocumentResource = class extends BaseResource {
|
|
|
397
646
|
() => this.http.post(`/accounts/${accId}/templates/${tmplId}/documents`, body)
|
|
398
647
|
);
|
|
399
648
|
}
|
|
400
|
-
/**
|
|
649
|
+
/**
|
|
650
|
+
* Estimate the credit cost of creating a document from a template.
|
|
651
|
+
*
|
|
652
|
+
* @returns an {@link ICostEstimate}: `total_credits`, balances, and a
|
|
653
|
+
* per-line `breakdown` of what the operation would consume.
|
|
654
|
+
*/
|
|
401
655
|
async estimateCostFromTemplate(templateId, signers, accountId) {
|
|
402
656
|
const tmplId = this.requireId(templateId, "Template ID");
|
|
403
657
|
const accId = this.accountId(accountId);
|
|
@@ -466,43 +720,6 @@ var DocumentResource = class extends BaseResource {
|
|
|
466
720
|
return { signed, total, pending, percentage };
|
|
467
721
|
}
|
|
468
722
|
};
|
|
469
|
-
async function loadSource(source) {
|
|
470
|
-
if ("buffer" in source) {
|
|
471
|
-
if (!source.fileName) {
|
|
472
|
-
throw new ValidationError("fileName is required when uploading a Buffer");
|
|
473
|
-
}
|
|
474
|
-
return { buffer: source.buffer, fileName: source.fileName };
|
|
475
|
-
}
|
|
476
|
-
if (!source.filePath) {
|
|
477
|
-
throw new ValidationError("filePath is required");
|
|
478
|
-
}
|
|
479
|
-
const buffer = await fs.readFile(source.filePath);
|
|
480
|
-
return { buffer, fileName: source.fileName ?? path.basename(source.filePath) };
|
|
481
|
-
}
|
|
482
|
-
function validateUpload(buffer, fileName) {
|
|
483
|
-
if (!buffer || buffer.byteLength === 0) {
|
|
484
|
-
throw new ValidationError("File buffer is empty", { fileName });
|
|
485
|
-
}
|
|
486
|
-
if (!fileName.toLowerCase().endsWith(".pdf")) {
|
|
487
|
-
throw new ValidationError("Only PDF files are supported", { fileName });
|
|
488
|
-
}
|
|
489
|
-
if (buffer.byteLength > MAX_UPLOAD_BYTES) {
|
|
490
|
-
throw new ValidationError("File size exceeds maximum allowed (25MB)", {
|
|
491
|
-
fileSize: buffer.byteLength,
|
|
492
|
-
maxSize: MAX_UPLOAD_BYTES
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
function buildUploadForm(buffer, fileName, metadata) {
|
|
497
|
-
const form = new FormData();
|
|
498
|
-
const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
499
|
-
form.append("file", new Blob([view], { type: "application/pdf" }), fileName);
|
|
500
|
-
form.append("name", fileName);
|
|
501
|
-
if (metadata) {
|
|
502
|
-
form.append("metadata", JSON.stringify(metadata));
|
|
503
|
-
}
|
|
504
|
-
return form;
|
|
505
|
-
}
|
|
506
723
|
function sleep(ms) {
|
|
507
724
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
508
725
|
}
|
|
@@ -541,7 +758,8 @@ var SignerResource = class extends BaseResource {
|
|
|
541
758
|
() => this.http.post(`/accounts/${id}/signers`, normaliseSignerPayload(payload))
|
|
542
759
|
);
|
|
543
760
|
} catch (err) {
|
|
544
|
-
|
|
761
|
+
const isDuplicateStatus = err instanceof ApiError && (err.statusCode === 409 || err.statusCode === 400);
|
|
762
|
+
if (isDuplicateStatus && payload.email) {
|
|
545
763
|
const duplicate = await this.findByEmail(payload.email, id);
|
|
546
764
|
if (duplicate) {
|
|
547
765
|
this.logger.info("Signer already exists, using existing signer", {
|
|
@@ -562,12 +780,12 @@ var SignerResource = class extends BaseResource {
|
|
|
562
780
|
() => this.http.get(`/accounts/${id}/signers/${sid}`)
|
|
563
781
|
);
|
|
564
782
|
}
|
|
565
|
-
/** List signers for the workspace (supports `page`, `
|
|
783
|
+
/** List signers for the workspace (supports `page`, `per-page`, `search`, `sort`). */
|
|
566
784
|
async list(params = {}, accountId) {
|
|
567
785
|
const id = this.accountId(accountId);
|
|
568
786
|
return this.callList(
|
|
569
787
|
"Failed to list signers",
|
|
570
|
-
() => this.http.get(`/accounts/${id}/signers`, { params:
|
|
788
|
+
() => this.http.get(`/accounts/${id}/signers`, { params: cleanListParams(params) })
|
|
571
789
|
);
|
|
572
790
|
}
|
|
573
791
|
/** Update a signer. Fails if the signer has active assignments. */
|
|
@@ -588,11 +806,28 @@ var SignerResource = class extends BaseResource {
|
|
|
588
806
|
() => this.http.delete(`/accounts/${id}/signers/${sid}`)
|
|
589
807
|
);
|
|
590
808
|
}
|
|
591
|
-
/**
|
|
809
|
+
/**
|
|
810
|
+
* Find a signer by exact email, using the API's `search` filter to narrow
|
|
811
|
+
* the page first. Returns `null` if none match.
|
|
812
|
+
*
|
|
813
|
+
* `search` is a substring match across signer fields, so the result is
|
|
814
|
+
* re-filtered here for an exact, case-insensitive email match.
|
|
815
|
+
*
|
|
816
|
+
* Page size is pinned to the API's maximum of 50: larger values are
|
|
817
|
+
* silently clamped to 50 by the server, so asking for more is misleading.
|
|
818
|
+
* An exact address realistically matches one signer, but a search term that
|
|
819
|
+
* matched more than 50 could in principle miss one — the API exposes no
|
|
820
|
+
* exact-email filter to rule that out.
|
|
821
|
+
*
|
|
822
|
+
* @param email - Exact email address to look for.
|
|
823
|
+
* @param accountId - Override the client's default account ID.
|
|
824
|
+
* @returns The matching {@link ISigner}, or `null`.
|
|
825
|
+
* @throws {ValidationError} If `email` is not a valid address.
|
|
826
|
+
*/
|
|
592
827
|
async findByEmail(email, accountId) {
|
|
593
828
|
this.assertEmail(email);
|
|
594
829
|
try {
|
|
595
|
-
const { data } = await this.list({ search: email,
|
|
830
|
+
const { data } = await this.list({ search: email, "per-page": 50 }, accountId);
|
|
596
831
|
const lower = email.toLowerCase();
|
|
597
832
|
return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
|
|
598
833
|
} catch (err) {
|
|
@@ -704,6 +939,58 @@ function normaliseSignerRef(ref, options) {
|
|
|
704
939
|
throw new ValidationError("Invalid signer reference", { ref });
|
|
705
940
|
}
|
|
706
941
|
var AssignmentResource = class extends BaseResource {
|
|
942
|
+
/**
|
|
943
|
+
* List assignments across the workspace (`GET /assignments`).
|
|
944
|
+
*
|
|
945
|
+
* The account is passed as an `accountId` **query parameter** — the API
|
|
946
|
+
* responds `400` ("Um contexto de conta é necessário e não foi fornecido")
|
|
947
|
+
* without it. Note the camelCase spelling: `account_id` and an
|
|
948
|
+
* `X-Account-Id` header are both rejected.
|
|
949
|
+
*
|
|
950
|
+
* @param params - `page`, `per-page`.
|
|
951
|
+
* @param accountId - Override the client's default account ID.
|
|
952
|
+
* @returns Assignments, with pagination in `meta`. Each item:
|
|
953
|
+
* ```jsonc
|
|
954
|
+
* {
|
|
955
|
+
* "id": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4",
|
|
956
|
+
* "sender_email": "sender@example.com",
|
|
957
|
+
* "method": "virtual",
|
|
958
|
+
* "expires_at": null,
|
|
959
|
+
* "message": "Please sign this contract",
|
|
960
|
+
* "signers": [
|
|
961
|
+
* {
|
|
962
|
+
* "id": "b2c3d4e5f6a7b8c9d0e1f2a3b4c5",
|
|
963
|
+
* "full_name": "Ana Souza",
|
|
964
|
+
* "email": "signer@example.com",
|
|
965
|
+
* "whatsapp_phone_number": null,
|
|
966
|
+
* "has_accepted_terms": false,
|
|
967
|
+
* "completed": false,
|
|
968
|
+
* "notification_history": [],
|
|
969
|
+
* "verification_method": "Email",
|
|
970
|
+
* "notification_methods": ["Email"],
|
|
971
|
+
* "step": 1,
|
|
972
|
+
* "notified": true
|
|
973
|
+
* }
|
|
974
|
+
* ]
|
|
975
|
+
* }
|
|
976
|
+
* ```
|
|
977
|
+
* @throws {ValidationError} If no account ID is available.
|
|
978
|
+
* @throws {ApiError} If the API rejects the request.
|
|
979
|
+
*
|
|
980
|
+
* @example
|
|
981
|
+
* ```ts
|
|
982
|
+
* const { data, meta } = await client.assignments.list({ 'per-page': 20 });
|
|
983
|
+
* ```
|
|
984
|
+
*/
|
|
985
|
+
async list(params = {}, accountId) {
|
|
986
|
+
const id = this.accountId(accountId);
|
|
987
|
+
return this.callList(
|
|
988
|
+
"Failed to list assignments",
|
|
989
|
+
() => this.http.get("/assignments", {
|
|
990
|
+
params: { accountId: id, ...cleanListParams(params) }
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
}
|
|
707
994
|
/** Create a signing assignment for a document. */
|
|
708
995
|
async create(documentId, payload) {
|
|
709
996
|
const docId = this.requireId(documentId, "Document ID");
|
|
@@ -718,7 +1005,15 @@ var AssignmentResource = class extends BaseResource {
|
|
|
718
1005
|
() => this.http.post(`/documents/${docId}/assignments`, body)
|
|
719
1006
|
);
|
|
720
1007
|
}
|
|
721
|
-
/**
|
|
1008
|
+
/**
|
|
1009
|
+
* Estimate the cost (in credits/documents) of creating the assignment.
|
|
1010
|
+
*
|
|
1011
|
+
* Signer entries may omit `id` and supply only `verification_method` /
|
|
1012
|
+
* `notification_methods` when only the channel mix matters for the estimate.
|
|
1013
|
+
*
|
|
1014
|
+
* @returns an {@link ICostEstimate} with `total_credits`, balances, and a
|
|
1015
|
+
* line-item `breakdown`.
|
|
1016
|
+
*/
|
|
722
1017
|
async estimateCost(documentId, payload) {
|
|
723
1018
|
const docId = this.requireId(documentId, "Document ID");
|
|
724
1019
|
return this.call(
|
|
@@ -753,7 +1048,11 @@ var AssignmentResource = class extends BaseResource {
|
|
|
753
1048
|
() => this.http.put(`/documents/${docId}/assignments/${asgId}/signers/${sid}/resend`)
|
|
754
1049
|
);
|
|
755
1050
|
}
|
|
756
|
-
/**
|
|
1051
|
+
/**
|
|
1052
|
+
* Estimate the cost of resending a signer notification.
|
|
1053
|
+
*
|
|
1054
|
+
* @returns an {@link IResendCostEstimate} (`total`, `breakdown`, balances).
|
|
1055
|
+
*/
|
|
757
1056
|
async estimateResendCost(documentId, assignmentId, signerId) {
|
|
758
1057
|
const docId = this.requireId(documentId, "Document ID");
|
|
759
1058
|
const asgId = this.requireId(assignmentId, "Assignment ID");
|
|
@@ -777,26 +1076,10 @@ var AssignmentResource = class extends BaseResource {
|
|
|
777
1076
|
() => this.http.get(`/documents/${docId}/assignments/${asgId}/whatsapp-notifications`)
|
|
778
1077
|
);
|
|
779
1078
|
}
|
|
780
|
-
/**
|
|
781
|
-
* Cancel a signature request. This endpoint is not listed in the public
|
|
782
|
-
* Swagger but is exposed by the platform.
|
|
783
|
-
*/
|
|
784
|
-
async cancel(documentId, reason, accountId) {
|
|
785
|
-
const docId = this.requireId(documentId, "Document ID");
|
|
786
|
-
const accId = this.accountId(accountId);
|
|
787
|
-
this.logger.info("Cancelling signature request", { documentId: docId, reason });
|
|
788
|
-
return this.call(
|
|
789
|
-
"Failed to cancel signature request",
|
|
790
|
-
() => this.http.post(
|
|
791
|
-
`/accounts/${accId}/signature-requests/${docId}/cancel`,
|
|
792
|
-
{ document_id: docId, reason }
|
|
793
|
-
)
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
1079
|
};
|
|
797
1080
|
|
|
798
1081
|
// src/resources/webhooks.ts
|
|
799
|
-
var
|
|
1082
|
+
var DEFAULT_WEBHOOK_EVENTS = [
|
|
800
1083
|
"document_ready",
|
|
801
1084
|
"document_prepared",
|
|
802
1085
|
"signer_signed_document",
|
|
@@ -804,7 +1087,21 @@ var DEFAULT_EVENTS = [
|
|
|
804
1087
|
"document_processing_failed"
|
|
805
1088
|
];
|
|
806
1089
|
var WebhookResource = class extends BaseResource {
|
|
807
|
-
/**
|
|
1090
|
+
/**
|
|
1091
|
+
* Register (or replace) the workspace's single webhook subscription
|
|
1092
|
+
* (`PUT /accounts/{id}/webhooks/subscriptions`). There is exactly one
|
|
1093
|
+
* subscription per workspace, keyed by URL.
|
|
1094
|
+
*
|
|
1095
|
+
* When `events` is omitted or empty, {@link DEFAULT_WEBHOOK_EVENTS} is used
|
|
1096
|
+
* (`document_ready`, `document_prepared`, `signer_signed_document`,
|
|
1097
|
+
* `signer_rejected_document`, `document_processing_failed`).
|
|
1098
|
+
*
|
|
1099
|
+
* @example
|
|
1100
|
+
* ```ts
|
|
1101
|
+
* await client.webhooks.register({ url: 'https://example.com/hook', email: 'ops@example.com' });
|
|
1102
|
+
* // → { url, email, events: [...], is_active: true, updated_at: '2026-…' }
|
|
1103
|
+
* ```
|
|
1104
|
+
*/
|
|
808
1105
|
async register(payload, accountId) {
|
|
809
1106
|
if (!payload.url) throw new ValidationError("Webhook URL is required");
|
|
810
1107
|
if (!payload.email) throw new ValidationError("Webhook email is required");
|
|
@@ -812,7 +1109,7 @@ var WebhookResource = class extends BaseResource {
|
|
|
812
1109
|
const body = {
|
|
813
1110
|
url: payload.url,
|
|
814
1111
|
email: payload.email,
|
|
815
|
-
events: payload.events && payload.events.length > 0 ? payload.events :
|
|
1112
|
+
events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_WEBHOOK_EVENTS,
|
|
816
1113
|
is_active: payload.is_active ?? true
|
|
817
1114
|
};
|
|
818
1115
|
this.logger.info("Registering webhook", { url: payload.url });
|
|
@@ -829,16 +1126,14 @@ var WebhookResource = class extends BaseResource {
|
|
|
829
1126
|
() => this.http.get(`/accounts/${id}/webhooks/subscriptions`)
|
|
830
1127
|
);
|
|
831
1128
|
}
|
|
832
|
-
/**
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
}
|
|
841
|
-
/** Inactivate the current webhook subscription without deleting it. */
|
|
1129
|
+
/**
|
|
1130
|
+
* Inactivate the current webhook subscription.
|
|
1131
|
+
*
|
|
1132
|
+
* This is the only supported way to stop deliveries — the API has no
|
|
1133
|
+
* subscription-delete route. The subscription is retained (with its `url`
|
|
1134
|
+
* and `events`) and simply stops firing; re-enable it by calling
|
|
1135
|
+
* {@link WebhookResource.register} again with `is_active: true`.
|
|
1136
|
+
*/
|
|
842
1137
|
async inactivate(accountId) {
|
|
843
1138
|
const id = this.accountId(accountId);
|
|
844
1139
|
this.logger.info("Inactivating webhook subscription");
|
|
@@ -860,7 +1155,7 @@ var WebhookResource = class extends BaseResource {
|
|
|
860
1155
|
return this.callList(
|
|
861
1156
|
"Failed to list webhook dispatches",
|
|
862
1157
|
() => this.http.get(`/accounts/${id}/webhooks`, {
|
|
863
|
-
params:
|
|
1158
|
+
params: cleanListParams(params)
|
|
864
1159
|
})
|
|
865
1160
|
);
|
|
866
1161
|
}
|
|
@@ -877,20 +1172,56 @@ var WebhookResource = class extends BaseResource {
|
|
|
877
1172
|
|
|
878
1173
|
// src/resources/templates.ts
|
|
879
1174
|
var TemplateResource = class extends BaseResource {
|
|
1175
|
+
/**
|
|
1176
|
+
* Create a template by uploading a PDF (`POST /accounts/{id}/templates`).
|
|
1177
|
+
*
|
|
1178
|
+
* The template is created in `Uploaded` status and transitions to `Ready`
|
|
1179
|
+
* once the platform finishes processing its pages. Configure roles/fields
|
|
1180
|
+
* afterwards in the Assinafy editor.
|
|
1181
|
+
*
|
|
1182
|
+
* @example
|
|
1183
|
+
* ```ts
|
|
1184
|
+
* const tmpl = await client.templates.create(
|
|
1185
|
+
* { filePath: './nda.pdf' },
|
|
1186
|
+
* { name: 'NDA template' },
|
|
1187
|
+
* );
|
|
1188
|
+
* // → { resource: 'template', id, name, status: 'Uploaded',
|
|
1189
|
+
* // roles: [{ id, name: 'TemplateEditor', assignment_type: 'Editor' }],
|
|
1190
|
+
* // pages: [], tags: [], created_at, updated_at }
|
|
1191
|
+
* ```
|
|
1192
|
+
*/
|
|
1193
|
+
async create(source, options = {}) {
|
|
1194
|
+
const id = this.accountId(options.accountId);
|
|
1195
|
+
const formOptions = {};
|
|
1196
|
+
if (options.name !== void 0) formOptions.name = options.name;
|
|
1197
|
+
const template = await this.uploadPdf(
|
|
1198
|
+
`/accounts/${id}/templates`,
|
|
1199
|
+
source,
|
|
1200
|
+
formOptions,
|
|
1201
|
+
{
|
|
1202
|
+
errorLabel: "Failed to create template",
|
|
1203
|
+
missingId: "Template upload succeeded but no template ID was returned"
|
|
1204
|
+
}
|
|
1205
|
+
);
|
|
1206
|
+
this.logger.info("Template created", { templateId: template.id });
|
|
1207
|
+
return template;
|
|
1208
|
+
}
|
|
880
1209
|
/** List templates for the workspace. */
|
|
881
1210
|
async list(params = {}, accountId) {
|
|
882
1211
|
const id = this.accountId(accountId);
|
|
883
1212
|
return this.callList(
|
|
884
1213
|
"Failed to list templates",
|
|
885
|
-
() => this.http.get(`/accounts/${id}/templates`, { params:
|
|
1214
|
+
() => this.http.get(`/accounts/${id}/templates`, { params: cleanListParams(params) })
|
|
886
1215
|
);
|
|
887
1216
|
}
|
|
888
1217
|
/**
|
|
889
|
-
* Get a template by ID.
|
|
1218
|
+
* Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
|
|
890
1219
|
*
|
|
891
|
-
*
|
|
892
|
-
* `
|
|
893
|
-
*
|
|
1220
|
+
* Returns the same shape as {@link TemplateResource.list} plus
|
|
1221
|
+
* `default_document_tags` (the tags auto-applied to every document created
|
|
1222
|
+
* from this template) and `resource`. Both endpoints return `pages` with
|
|
1223
|
+
* per-page `download_url`, so fetching a template again purely to read its
|
|
1224
|
+
* pages is unnecessary.
|
|
894
1225
|
*/
|
|
895
1226
|
async get(templateId, accountId) {
|
|
896
1227
|
const id = this.accountId(accountId);
|
|
@@ -901,9 +1232,40 @@ var TemplateResource = class extends BaseResource {
|
|
|
901
1232
|
);
|
|
902
1233
|
}
|
|
903
1234
|
/**
|
|
904
|
-
* `
|
|
905
|
-
*
|
|
906
|
-
*
|
|
1235
|
+
* Update a template's `name` and/or default `message`
|
|
1236
|
+
* (`PUT /accounts/{id}/templates/{template_id}`). Returns the updated template.
|
|
1237
|
+
*
|
|
1238
|
+
* @example
|
|
1239
|
+
* ```ts
|
|
1240
|
+
* await client.templates.update(templateId, { name: 'NDA v2', message: 'Please sign' });
|
|
1241
|
+
* ```
|
|
1242
|
+
*/
|
|
1243
|
+
async update(templateId, payload, accountId) {
|
|
1244
|
+
const id = this.accountId(accountId);
|
|
1245
|
+
const tmplId = this.requireId(templateId, "Template ID");
|
|
1246
|
+
return this.call(
|
|
1247
|
+
"Failed to update template",
|
|
1248
|
+
() => this.http.put(
|
|
1249
|
+
`/accounts/${id}/templates/${tmplId}`,
|
|
1250
|
+
cleanParams(payload)
|
|
1251
|
+
)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
/** Delete a template (`DELETE /accounts/{id}/templates/{template_id}`). */
|
|
1255
|
+
async delete(templateId, accountId) {
|
|
1256
|
+
const id = this.accountId(accountId);
|
|
1257
|
+
const tmplId = this.requireId(templateId, "Template ID");
|
|
1258
|
+
return this.callVoid(
|
|
1259
|
+
"Failed to delete template",
|
|
1260
|
+
() => this.http.delete(`/accounts/${id}/templates/${tmplId}`)
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Download a template page as a JPEG
|
|
1265
|
+
* (`GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`).
|
|
1266
|
+
*
|
|
1267
|
+
* Used by template editors to render page thumbnails on the client. The
|
|
1268
|
+
* matching `download_url` is also returned on each `template.pages[]` entry.
|
|
907
1269
|
*/
|
|
908
1270
|
async downloadPage(templateId, pageId, accountId) {
|
|
909
1271
|
const id = this.accountId(accountId);
|
|
@@ -927,7 +1289,7 @@ var TagResource = class extends BaseResource {
|
|
|
927
1289
|
return this.call(
|
|
928
1290
|
"Failed to list tags",
|
|
929
1291
|
() => this.http.get(`/accounts/${id}/tags`, {
|
|
930
|
-
params:
|
|
1292
|
+
params: cleanListParams(params)
|
|
931
1293
|
})
|
|
932
1294
|
);
|
|
933
1295
|
}
|
|
@@ -1069,7 +1431,7 @@ var FieldsResource = class extends BaseResource {
|
|
|
1069
1431
|
return this.call(
|
|
1070
1432
|
"Failed to list field definitions",
|
|
1071
1433
|
() => this.http.get(`/accounts/${id}/fields`, {
|
|
1072
|
-
params:
|
|
1434
|
+
params: cleanListParams(params)
|
|
1073
1435
|
})
|
|
1074
1436
|
);
|
|
1075
1437
|
}
|
|
@@ -1153,7 +1515,44 @@ var SignerDocumentsResource = class extends BaseResource {
|
|
|
1153
1515
|
return this.callList(
|
|
1154
1516
|
"Failed to list signer documents",
|
|
1155
1517
|
() => this.http.get(`/signers/${sid}/documents`, {
|
|
1156
|
-
params: { "signer-access-code": code, ...
|
|
1518
|
+
params: { "signer-access-code": code, ...cleanListParams(params) }
|
|
1519
|
+
})
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Search the documents awaiting a given signer
|
|
1524
|
+
* (`GET /signers/{signer_id}/documents/search?signer-access-code=…`).
|
|
1525
|
+
*
|
|
1526
|
+
* The signer-side counterpart of {@link DocumentResource.search}, scoped to
|
|
1527
|
+
* one signer and authorised by their access code rather than the API key.
|
|
1528
|
+
* Like {@link SignerDocumentsResource.list}, it requires
|
|
1529
|
+
* `signer-access-code`; the published spec omits that parameter, but the
|
|
1530
|
+
* endpoint is not usable without it.
|
|
1531
|
+
*
|
|
1532
|
+
* @param signerId - The signer whose documents are searched.
|
|
1533
|
+
* @param signerAccessCode - The signer's access code, from their signing link.
|
|
1534
|
+
* @param search - Free-text term matched against the document name.
|
|
1535
|
+
* @returns Matching documents for that signer, in the compact
|
|
1536
|
+
* {@link IDocumentListItem} shape, with pagination in `meta`.
|
|
1537
|
+
* @throws {ValidationError} If `signerId` or `signerAccessCode` is missing.
|
|
1538
|
+
* @throws {ApiError} If the access code is invalid or expired.
|
|
1539
|
+
*
|
|
1540
|
+
* @example
|
|
1541
|
+
* ```ts
|
|
1542
|
+
* const { data } = await client.signerDocuments.search(
|
|
1543
|
+
* signerId,
|
|
1544
|
+
* accessCode,
|
|
1545
|
+
* 'agreement',
|
|
1546
|
+
* );
|
|
1547
|
+
* ```
|
|
1548
|
+
*/
|
|
1549
|
+
async search(signerId, signerAccessCode, search) {
|
|
1550
|
+
const sid = this.requireId(signerId, "Signer ID");
|
|
1551
|
+
const code = this.requireId(signerAccessCode, "signer-access-code");
|
|
1552
|
+
return this.callList(
|
|
1553
|
+
"Failed to search signer documents",
|
|
1554
|
+
() => this.http.get(`/signers/${sid}/documents/search`, {
|
|
1555
|
+
params: cleanParams({ "signer-access-code": code, search })
|
|
1157
1556
|
})
|
|
1158
1557
|
);
|
|
1159
1558
|
}
|
|
@@ -1271,7 +1670,12 @@ var SignerDocumentsResource = class extends BaseResource {
|
|
|
1271
1670
|
})
|
|
1272
1671
|
);
|
|
1273
1672
|
}
|
|
1274
|
-
/**
|
|
1673
|
+
/**
|
|
1674
|
+
* `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it.
|
|
1675
|
+
*
|
|
1676
|
+
* @param hasAcceptedTerms maps to the `has_accepted_terms` query param
|
|
1677
|
+
* (server default `false`); pass `true` once the signer has accepted terms.
|
|
1678
|
+
*/
|
|
1275
1679
|
async getAssignment(signerAccessCode, hasAcceptedTerms) {
|
|
1276
1680
|
const code = this.requireId(signerAccessCode, "signer-access-code");
|
|
1277
1681
|
return this.call(
|
|
@@ -1301,8 +1705,8 @@ var SignerDocumentsResource = class extends BaseResource {
|
|
|
1301
1705
|
}
|
|
1302
1706
|
/**
|
|
1303
1707
|
* `PUT /documents/{documentId}/assignments/{assignmentId}/reject?signer-access-code=…`
|
|
1304
|
-
* — signer-side decline. (
|
|
1305
|
-
* workspace
|
|
1708
|
+
* — signer-side decline. (The workspace-side equivalent is to delete the
|
|
1709
|
+
* document via `documents.delete`; there is no workspace "cancel" endpoint.)
|
|
1306
1710
|
*/
|
|
1307
1711
|
async decline(documentId, assignmentId, signerAccessCode, declineReason) {
|
|
1308
1712
|
const did = this.requireId(documentId, "Document ID");
|
|
@@ -1368,6 +1772,21 @@ var WebhookVerifier = class {
|
|
|
1368
1772
|
// src/client.ts
|
|
1369
1773
|
var DEFAULT_BASE_URL = "https://api.assinafy.com.br/v1";
|
|
1370
1774
|
var AssinafyClient = class _AssinafyClient {
|
|
1775
|
+
axiosInstance;
|
|
1776
|
+
defaultAccountId;
|
|
1777
|
+
logger;
|
|
1778
|
+
webhookSecret;
|
|
1779
|
+
documents;
|
|
1780
|
+
signers;
|
|
1781
|
+
workspaces;
|
|
1782
|
+
assignments;
|
|
1783
|
+
webhooks;
|
|
1784
|
+
templates;
|
|
1785
|
+
tags;
|
|
1786
|
+
auth;
|
|
1787
|
+
fields;
|
|
1788
|
+
signerDocuments;
|
|
1789
|
+
webhookVerifier;
|
|
1371
1790
|
constructor(options) {
|
|
1372
1791
|
if (!options.apiKey && !options.token) {
|
|
1373
1792
|
throw new ValidationError(
|
|
@@ -1393,6 +1812,10 @@ var AssinafyClient = class _AssinafyClient {
|
|
|
1393
1812
|
timeout: options.timeout ?? 3e4,
|
|
1394
1813
|
headers
|
|
1395
1814
|
});
|
|
1815
|
+
const maxRetries = options.maxRetries ?? 2;
|
|
1816
|
+
if (maxRetries > 0) {
|
|
1817
|
+
installRateLimitRetry(this.axiosInstance, maxRetries, this.logger);
|
|
1818
|
+
}
|
|
1396
1819
|
this.documents = new DocumentResource(this.axiosInstance, this.defaultAccountId, this.logger);
|
|
1397
1820
|
this.signers = new SignerResource(this.axiosInstance, this.defaultAccountId, this.logger);
|
|
1398
1821
|
this.workspaces = new WorkspaceResource(this.axiosInstance, void 0, this.logger);
|
|
@@ -1431,6 +1854,7 @@ var AssinafyClient = class _AssinafyClient {
|
|
|
1431
1854
|
if (baseUrl !== void 0) opts.baseUrl = baseUrl;
|
|
1432
1855
|
if (webhookSecret !== void 0) opts.webhookSecret = webhookSecret;
|
|
1433
1856
|
if (config.timeout !== void 0) opts.timeout = config.timeout;
|
|
1857
|
+
if (config.maxRetries !== void 0) opts.maxRetries = config.maxRetries;
|
|
1434
1858
|
if (config.logger !== void 0) opts.logger = config.logger;
|
|
1435
1859
|
return new _AssinafyClient(opts);
|
|
1436
1860
|
}
|
|
@@ -1490,14 +1914,37 @@ var AssinafyClient = class _AssinafyClient {
|
|
|
1490
1914
|
function normaliseBaseUrl(raw) {
|
|
1491
1915
|
return raw.endsWith("/") ? raw.slice(0, -1) : raw;
|
|
1492
1916
|
}
|
|
1917
|
+
function installRateLimitRetry(http, maxRetries, logger) {
|
|
1918
|
+
http.interceptors.response.use(
|
|
1919
|
+
(response) => response,
|
|
1920
|
+
async (error) => {
|
|
1921
|
+
if (!axios2.isAxiosError(error) || error.response?.status !== 429 || !error.config) {
|
|
1922
|
+
throw error;
|
|
1923
|
+
}
|
|
1924
|
+
const config = error.config;
|
|
1925
|
+
const attempt = (config._retryCount ?? 0) + 1;
|
|
1926
|
+
if (attempt > maxRetries) throw error;
|
|
1927
|
+
config._retryCount = attempt;
|
|
1928
|
+
const delayMs = nextRetryDelayMs(
|
|
1929
|
+
error.response.headers,
|
|
1930
|
+
attempt
|
|
1931
|
+
);
|
|
1932
|
+
logger.warn("Rate limited (429); retrying after delay", { attempt, maxRetries, delayMs });
|
|
1933
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1934
|
+
return http(config);
|
|
1935
|
+
}
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1493
1938
|
export {
|
|
1494
1939
|
ApiError,
|
|
1495
1940
|
AssignmentResource,
|
|
1496
1941
|
AssinafyClient,
|
|
1497
1942
|
AssinafyError,
|
|
1498
1943
|
AuthenticationResource,
|
|
1944
|
+
DEFAULT_WEBHOOK_EVENTS,
|
|
1499
1945
|
DocumentResource,
|
|
1500
1946
|
FieldsResource,
|
|
1947
|
+
MAX_UPLOAD_BYTES,
|
|
1501
1948
|
NetworkError,
|
|
1502
1949
|
SignerDocumentsResource,
|
|
1503
1950
|
SignerResource,
|