@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/dist/index.js CHANGED
@@ -35,8 +35,10 @@ __export(index_exports, {
35
35
  AssinafyClient: () => AssinafyClient,
36
36
  AssinafyError: () => AssinafyError,
37
37
  AuthenticationResource: () => AuthenticationResource,
38
+ DEFAULT_WEBHOOK_EVENTS: () => DEFAULT_WEBHOOK_EVENTS,
38
39
  DocumentResource: () => DocumentResource,
39
40
  FieldsResource: () => FieldsResource,
41
+ MAX_UPLOAD_BYTES: () => MAX_UPLOAD_BYTES,
40
42
  NetworkError: () => NetworkError,
41
43
  SignerDocumentsResource: () => SignerDocumentsResource,
42
44
  SignerResource: () => SignerResource,
@@ -55,16 +57,16 @@ var import_axios2 = __toESM(require("axios"));
55
57
 
56
58
  // src/errors.ts
57
59
  var AssinafyError = class extends Error {
60
+ context;
58
61
  constructor(message, context = {}, options) {
59
- super(message);
62
+ super(message, options);
60
63
  this.name = "AssinafyError";
61
64
  this.context = context;
62
- if (options?.cause !== void 0) {
63
- this.cause = options.cause;
64
- }
65
65
  }
66
66
  };
67
67
  var ApiError = class _ApiError extends AssinafyError {
68
+ statusCode;
69
+ responseData;
68
70
  constructor(message, statusCode, responseData = null, options) {
69
71
  super(message, { statusCode, responseData }, options);
70
72
  this.name = "ApiError";
@@ -80,6 +82,7 @@ var ApiError = class _ApiError extends AssinafyError {
80
82
  }
81
83
  };
82
84
  var ValidationError = class extends AssinafyError {
85
+ errors;
83
86
  constructor(message = "Validation failed", errors = {}) {
84
87
  super(message, { errors });
85
88
  this.name = "ValidationError";
@@ -105,6 +108,23 @@ function handleAssinafyResponse(response) {
105
108
  }
106
109
  return response;
107
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
+ }
108
128
  function toSdkError(error, fallbackMessage) {
109
129
  if (error instanceof AssinafyError) {
110
130
  return error;
@@ -112,14 +132,15 @@ function toSdkError(error, fallbackMessage) {
112
132
  if (import_axios.default.isAxiosError(error)) {
113
133
  const status = error.response?.status;
114
134
  if (status) {
115
- return ApiError.fromResponse(status, error.response?.data ?? null);
135
+ const body = decodeBinaryErrorBody(error.response?.data ?? null);
136
+ return ApiError.fromResponse(status, body ?? null);
116
137
  }
117
138
  return new NetworkError(`${fallbackMessage}: ${error.message}`, { cause: error });
118
139
  }
119
140
  if (error instanceof Error) {
120
141
  return new AssinafyError(`${fallbackMessage}: ${error.message}`, {}, { cause: error });
121
142
  }
122
- return new AssinafyError(fallbackMessage, { cause: error });
143
+ return new AssinafyError(fallbackMessage, {}, { cause: error });
123
144
  }
124
145
  function createNoopLogger() {
125
146
  return {
@@ -138,12 +159,101 @@ function cleanParams(params) {
138
159
  }
139
160
  return out;
140
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
+ }
141
170
 
142
- // src/resources/documents.ts
171
+ // src/support/retry.ts
172
+ function header(headers, name) {
173
+ if (!headers) return void 0;
174
+ const lower = name.toLowerCase();
175
+ for (const [key, value] of Object.entries(headers)) {
176
+ if (key.toLowerCase() === lower && value != null) {
177
+ return Array.isArray(value) ? String(value[0]) : String(value);
178
+ }
179
+ }
180
+ return void 0;
181
+ }
182
+ function retryDelayFromHeaders(headers) {
183
+ const retryAfter = header(headers, "retry-after");
184
+ if (retryAfter !== void 0) {
185
+ const seconds = Number(retryAfter);
186
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
187
+ const date = Date.parse(retryAfter);
188
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
189
+ }
190
+ const reset = header(headers, "x-rate-limit-reset");
191
+ if (reset !== void 0) {
192
+ const seconds = Number(reset);
193
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
194
+ }
195
+ return void 0;
196
+ }
197
+ function backoffMs(attempt) {
198
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 8e3);
199
+ }
200
+ function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
201
+ const hinted = retryDelayFromHeaders(headers);
202
+ if (hinted !== void 0) return Math.min(hinted, maxDelayMs);
203
+ return Math.min(backoffMs(attempt), maxDelayMs);
204
+ }
205
+
206
+ // src/resources/upload.ts
143
207
  var import_node_fs = require("fs");
144
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
+ }
145
254
 
146
255
  // src/resources/base.ts
256
+ var MULTIPART_CONTENT_TYPE = "multipart/form-data";
147
257
  var BaseResource = class {
148
258
  constructor(http, defaultAccountId, logger = createNoopLogger()) {
149
259
  this.http = http;
@@ -205,6 +315,35 @@ var BaseResource = class {
205
315
  throw toSdkError(err, label);
206
316
  }
207
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
+ }
208
347
  /** Execute a paginated list call and attach meta from `X-Pagination-*` headers. */
209
348
  async callList(label, request) {
210
349
  try {
@@ -247,7 +386,6 @@ function toInt(value) {
247
386
  }
248
387
 
249
388
  // src/resources/documents.ts
250
- var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
251
389
  var READY_STATUSES = /* @__PURE__ */ new Set([
252
390
  "metadata_ready",
253
391
  "pending_signature",
@@ -261,43 +399,153 @@ var FAILED_STATUSES = /* @__PURE__ */ new Set([
261
399
  ]);
262
400
  var DocumentResource = class extends BaseResource {
263
401
  /**
264
- * 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.
265
426
  *
266
427
  * @example
267
428
  * ```ts
268
429
  * await client.documents.upload({ filePath: './contract.pdf' });
269
- * await client.documents.upload({ buffer, fileName: 'contract.pdf' }, { metadata });
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'
270
435
  * ```
271
436
  */
272
437
  async upload(source, options = {}) {
273
- const { buffer, fileName } = await loadSource(source);
274
- validateUpload(buffer, fileName);
275
438
  const accountId = this.accountId(options.accountId);
276
- const form = buildUploadForm(buffer, fileName, options.metadata);
277
- this.logger.info("Uploading document", { fileName, size: buffer.byteLength });
278
- const document = await this.call(
279
- "Document upload failed",
280
- () => this.http.post(`/accounts/${accountId}/documents`, form, {
281
- headers: { "Content-Type": "multipart/form-data" }
282
- })
439
+ const formOptions = {};
440
+ if (options.name !== void 0) formOptions.name = options.name;
441
+ if (options.metadata !== void 0) formOptions.metadata = options.metadata;
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
+ }
283
450
  );
284
- if (!document?.id) {
285
- throw new ValidationError("Upload succeeded but no document ID was returned", {
286
- response: document
287
- });
288
- }
289
451
  this.logger.info("Document uploaded", { documentId: document.id });
290
452
  return document;
291
453
  }
292
454
  /**
293
455
  * List workspace documents. Pagination info (if any) is attached in `meta`.
294
- * Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per_page`.
456
+ * Supports `status`, `method`, `tags`, `search`, `sort`, `page`, `per-page`.
295
457
  */
296
458
  async list(params = {}, accountId) {
297
459
  const id = this.accountId(accountId);
298
460
  return this.callList(
299
461
  "Failed to list documents",
300
- () => this.http.get(`/accounts/${id}/documents`, { params: cleanParams(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 })
301
549
  );
302
550
  }
303
551
  /** Get document details. */
@@ -331,6 +579,9 @@ var DocumentResource = class extends BaseResource {
331
579
  }
332
580
  } catch (err) {
333
581
  if (err instanceof ValidationError) throw err;
582
+ if (err instanceof ApiError && err.statusCode < 500 && err.statusCode !== 429) {
583
+ throw err;
584
+ }
334
585
  this.logger.warn("Error checking document status", {
335
586
  error: err instanceof Error ? err.message : String(err)
336
587
  });
@@ -449,7 +700,12 @@ var DocumentResource = class extends BaseResource {
449
700
  () => this.http.post(`/accounts/${accId}/templates/${tmplId}/documents`, body)
450
701
  );
451
702
  }
452
- /** Estimate the credit cost of creating a document from a template. */
703
+ /**
704
+ * Estimate the credit cost of creating a document from a template.
705
+ *
706
+ * @returns an {@link ICostEstimate}: `total_credits`, balances, and a
707
+ * per-line `breakdown` of what the operation would consume.
708
+ */
453
709
  async estimateCostFromTemplate(templateId, signers, accountId) {
454
710
  const tmplId = this.requireId(templateId, "Template ID");
455
711
  const accId = this.accountId(accountId);
@@ -518,43 +774,6 @@ var DocumentResource = class extends BaseResource {
518
774
  return { signed, total, pending, percentage };
519
775
  }
520
776
  };
521
- async function loadSource(source) {
522
- if ("buffer" in source) {
523
- if (!source.fileName) {
524
- throw new ValidationError("fileName is required when uploading a Buffer");
525
- }
526
- return { buffer: source.buffer, fileName: source.fileName };
527
- }
528
- if (!source.filePath) {
529
- throw new ValidationError("filePath is required");
530
- }
531
- const buffer = await import_node_fs.promises.readFile(source.filePath);
532
- return { buffer, fileName: source.fileName ?? import_node_path.default.basename(source.filePath) };
533
- }
534
- function validateUpload(buffer, fileName) {
535
- if (!buffer || buffer.byteLength === 0) {
536
- throw new ValidationError("File buffer is empty", { fileName });
537
- }
538
- if (!fileName.toLowerCase().endsWith(".pdf")) {
539
- throw new ValidationError("Only PDF files are supported", { fileName });
540
- }
541
- if (buffer.byteLength > MAX_UPLOAD_BYTES) {
542
- throw new ValidationError("File size exceeds maximum allowed (25MB)", {
543
- fileSize: buffer.byteLength,
544
- maxSize: MAX_UPLOAD_BYTES
545
- });
546
- }
547
- }
548
- function buildUploadForm(buffer, fileName, metadata) {
549
- const form = new FormData();
550
- const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
551
- form.append("file", new Blob([view], { type: "application/pdf" }), fileName);
552
- form.append("name", fileName);
553
- if (metadata) {
554
- form.append("metadata", JSON.stringify(metadata));
555
- }
556
- return form;
557
- }
558
777
  function sleep(ms) {
559
778
  return new Promise((resolve) => setTimeout(resolve, ms));
560
779
  }
@@ -593,7 +812,8 @@ var SignerResource = class extends BaseResource {
593
812
  () => this.http.post(`/accounts/${id}/signers`, normaliseSignerPayload(payload))
594
813
  );
595
814
  } catch (err) {
596
- if (err instanceof ApiError && err.statusCode === 409 && payload.email) {
815
+ const isDuplicateStatus = err instanceof ApiError && (err.statusCode === 409 || err.statusCode === 400);
816
+ if (isDuplicateStatus && payload.email) {
597
817
  const duplicate = await this.findByEmail(payload.email, id);
598
818
  if (duplicate) {
599
819
  this.logger.info("Signer already exists, using existing signer", {
@@ -614,12 +834,12 @@ var SignerResource = class extends BaseResource {
614
834
  () => this.http.get(`/accounts/${id}/signers/${sid}`)
615
835
  );
616
836
  }
617
- /** List signers for the workspace (supports `page`, `per_page`, `search`, `sort`). */
837
+ /** List signers for the workspace (supports `page`, `per-page`, `search`, `sort`). */
618
838
  async list(params = {}, accountId) {
619
839
  const id = this.accountId(accountId);
620
840
  return this.callList(
621
841
  "Failed to list signers",
622
- () => this.http.get(`/accounts/${id}/signers`, { params: cleanParams(params) })
842
+ () => this.http.get(`/accounts/${id}/signers`, { params: cleanListParams(params) })
623
843
  );
624
844
  }
625
845
  /** Update a signer. Fails if the signer has active assignments. */
@@ -640,11 +860,28 @@ var SignerResource = class extends BaseResource {
640
860
  () => this.http.delete(`/accounts/${id}/signers/${sid}`)
641
861
  );
642
862
  }
643
- /** Find a signer by email via the API's `search` parameter. Returns `null` if none match. */
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
+ */
644
881
  async findByEmail(email, accountId) {
645
882
  this.assertEmail(email);
646
883
  try {
647
- const { data } = await this.list({ search: email, per_page: 100 }, accountId);
884
+ const { data } = await this.list({ search: email, "per-page": 50 }, accountId);
648
885
  const lower = email.toLowerCase();
649
886
  return data.find((s) => (s.email ?? "").toLowerCase() === lower) ?? null;
650
887
  } catch (err) {
@@ -756,6 +993,58 @@ function normaliseSignerRef(ref, options) {
756
993
  throw new ValidationError("Invalid signer reference", { ref });
757
994
  }
758
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
+ }
759
1048
  /** Create a signing assignment for a document. */
760
1049
  async create(documentId, payload) {
761
1050
  const docId = this.requireId(documentId, "Document ID");
@@ -770,7 +1059,15 @@ var AssignmentResource = class extends BaseResource {
770
1059
  () => this.http.post(`/documents/${docId}/assignments`, body)
771
1060
  );
772
1061
  }
773
- /** Estimate the cost (in credits) of creating the assignment. */
1062
+ /**
1063
+ * Estimate the cost (in credits/documents) of creating the assignment.
1064
+ *
1065
+ * Signer entries may omit `id` and supply only `verification_method` /
1066
+ * `notification_methods` when only the channel mix matters for the estimate.
1067
+ *
1068
+ * @returns an {@link ICostEstimate} with `total_credits`, balances, and a
1069
+ * line-item `breakdown`.
1070
+ */
774
1071
  async estimateCost(documentId, payload) {
775
1072
  const docId = this.requireId(documentId, "Document ID");
776
1073
  return this.call(
@@ -805,7 +1102,11 @@ var AssignmentResource = class extends BaseResource {
805
1102
  () => this.http.put(`/documents/${docId}/assignments/${asgId}/signers/${sid}/resend`)
806
1103
  );
807
1104
  }
808
- /** Estimate the cost of resending a signer notification. */
1105
+ /**
1106
+ * Estimate the cost of resending a signer notification.
1107
+ *
1108
+ * @returns an {@link IResendCostEstimate} (`total`, `breakdown`, balances).
1109
+ */
809
1110
  async estimateResendCost(documentId, assignmentId, signerId) {
810
1111
  const docId = this.requireId(documentId, "Document ID");
811
1112
  const asgId = this.requireId(assignmentId, "Assignment ID");
@@ -829,26 +1130,10 @@ var AssignmentResource = class extends BaseResource {
829
1130
  () => this.http.get(`/documents/${docId}/assignments/${asgId}/whatsapp-notifications`)
830
1131
  );
831
1132
  }
832
- /**
833
- * Cancel a signature request. This endpoint is not listed in the public
834
- * Swagger but is exposed by the platform.
835
- */
836
- async cancel(documentId, reason, accountId) {
837
- const docId = this.requireId(documentId, "Document ID");
838
- const accId = this.accountId(accountId);
839
- this.logger.info("Cancelling signature request", { documentId: docId, reason });
840
- return this.call(
841
- "Failed to cancel signature request",
842
- () => this.http.post(
843
- `/accounts/${accId}/signature-requests/${docId}/cancel`,
844
- { document_id: docId, reason }
845
- )
846
- );
847
- }
848
1133
  };
849
1134
 
850
1135
  // src/resources/webhooks.ts
851
- var DEFAULT_EVENTS = [
1136
+ var DEFAULT_WEBHOOK_EVENTS = [
852
1137
  "document_ready",
853
1138
  "document_prepared",
854
1139
  "signer_signed_document",
@@ -856,7 +1141,21 @@ var DEFAULT_EVENTS = [
856
1141
  "document_processing_failed"
857
1142
  ];
858
1143
  var WebhookResource = class extends BaseResource {
859
- /** Register (or replace) the webhook subscription for the workspace. */
1144
+ /**
1145
+ * Register (or replace) the workspace's single webhook subscription
1146
+ * (`PUT /accounts/{id}/webhooks/subscriptions`). There is exactly one
1147
+ * subscription per workspace, keyed by URL.
1148
+ *
1149
+ * When `events` is omitted or empty, {@link DEFAULT_WEBHOOK_EVENTS} is used
1150
+ * (`document_ready`, `document_prepared`, `signer_signed_document`,
1151
+ * `signer_rejected_document`, `document_processing_failed`).
1152
+ *
1153
+ * @example
1154
+ * ```ts
1155
+ * await client.webhooks.register({ url: 'https://example.com/hook', email: 'ops@example.com' });
1156
+ * // → { url, email, events: [...], is_active: true, updated_at: '2026-…' }
1157
+ * ```
1158
+ */
860
1159
  async register(payload, accountId) {
861
1160
  if (!payload.url) throw new ValidationError("Webhook URL is required");
862
1161
  if (!payload.email) throw new ValidationError("Webhook email is required");
@@ -864,7 +1163,7 @@ var WebhookResource = class extends BaseResource {
864
1163
  const body = {
865
1164
  url: payload.url,
866
1165
  email: payload.email,
867
- events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_EVENTS,
1166
+ events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_WEBHOOK_EVENTS,
868
1167
  is_active: payload.is_active ?? true
869
1168
  };
870
1169
  this.logger.info("Registering webhook", { url: payload.url });
@@ -881,16 +1180,14 @@ var WebhookResource = class extends BaseResource {
881
1180
  () => this.http.get(`/accounts/${id}/webhooks/subscriptions`)
882
1181
  );
883
1182
  }
884
- /** Delete the current webhook subscription. */
885
- async delete(accountId) {
886
- const id = this.accountId(accountId);
887
- this.logger.info("Deleting webhook subscription");
888
- return this.callVoid(
889
- "Failed to delete webhook subscription",
890
- () => this.http.delete(`/accounts/${id}/webhooks/subscriptions`)
891
- );
892
- }
893
- /** 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
+ */
894
1191
  async inactivate(accountId) {
895
1192
  const id = this.accountId(accountId);
896
1193
  this.logger.info("Inactivating webhook subscription");
@@ -912,7 +1209,7 @@ var WebhookResource = class extends BaseResource {
912
1209
  return this.callList(
913
1210
  "Failed to list webhook dispatches",
914
1211
  () => this.http.get(`/accounts/${id}/webhooks`, {
915
- params: cleanParams(params)
1212
+ params: cleanListParams(params)
916
1213
  })
917
1214
  );
918
1215
  }
@@ -929,20 +1226,56 @@ var WebhookResource = class extends BaseResource {
929
1226
 
930
1227
  // src/resources/templates.ts
931
1228
  var TemplateResource = class extends BaseResource {
1229
+ /**
1230
+ * Create a template by uploading a PDF (`POST /accounts/{id}/templates`).
1231
+ *
1232
+ * The template is created in `Uploaded` status and transitions to `Ready`
1233
+ * once the platform finishes processing its pages. Configure roles/fields
1234
+ * afterwards in the Assinafy editor.
1235
+ *
1236
+ * @example
1237
+ * ```ts
1238
+ * const tmpl = await client.templates.create(
1239
+ * { filePath: './nda.pdf' },
1240
+ * { name: 'NDA template' },
1241
+ * );
1242
+ * // → { resource: 'template', id, name, status: 'Uploaded',
1243
+ * // roles: [{ id, name: 'TemplateEditor', assignment_type: 'Editor' }],
1244
+ * // pages: [], tags: [], created_at, updated_at }
1245
+ * ```
1246
+ */
1247
+ async create(source, options = {}) {
1248
+ const id = this.accountId(options.accountId);
1249
+ const formOptions = {};
1250
+ if (options.name !== void 0) formOptions.name = options.name;
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
+ }
1259
+ );
1260
+ this.logger.info("Template created", { templateId: template.id });
1261
+ return template;
1262
+ }
932
1263
  /** List templates for the workspace. */
933
1264
  async list(params = {}, accountId) {
934
1265
  const id = this.accountId(accountId);
935
1266
  return this.callList(
936
1267
  "Failed to list templates",
937
- () => this.http.get(`/accounts/${id}/templates`, { params: cleanParams(params) })
1268
+ () => this.http.get(`/accounts/${id}/templates`, { params: cleanListParams(params) })
938
1269
  );
939
1270
  }
940
1271
  /**
941
- * Get a template by ID.
1272
+ * Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
942
1273
  *
943
- * Note: the swagger only documents the list endpoint; this single-resource
944
- * `GET /accounts/{id}/templates/{id}` is exposed by the platform and used
945
- * by the official PHP SDK.
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.
946
1279
  */
947
1280
  async get(templateId, accountId) {
948
1281
  const id = this.accountId(accountId);
@@ -953,9 +1286,40 @@ var TemplateResource = class extends BaseResource {
953
1286
  );
954
1287
  }
955
1288
  /**
956
- * `GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`
957
- * download a template page as a JPEG (used by template editors to render
958
- * thumbnails on the client).
1289
+ * Update a template's `name` and/or default `message`
1290
+ * (`PUT /accounts/{id}/templates/{template_id}`). Returns the updated template.
1291
+ *
1292
+ * @example
1293
+ * ```ts
1294
+ * await client.templates.update(templateId, { name: 'NDA v2', message: 'Please sign' });
1295
+ * ```
1296
+ */
1297
+ async update(templateId, payload, accountId) {
1298
+ const id = this.accountId(accountId);
1299
+ const tmplId = this.requireId(templateId, "Template ID");
1300
+ return this.call(
1301
+ "Failed to update template",
1302
+ () => this.http.put(
1303
+ `/accounts/${id}/templates/${tmplId}`,
1304
+ cleanParams(payload)
1305
+ )
1306
+ );
1307
+ }
1308
+ /** Delete a template (`DELETE /accounts/{id}/templates/{template_id}`). */
1309
+ async delete(templateId, accountId) {
1310
+ const id = this.accountId(accountId);
1311
+ const tmplId = this.requireId(templateId, "Template ID");
1312
+ return this.callVoid(
1313
+ "Failed to delete template",
1314
+ () => this.http.delete(`/accounts/${id}/templates/${tmplId}`)
1315
+ );
1316
+ }
1317
+ /**
1318
+ * Download a template page as a JPEG
1319
+ * (`GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`).
1320
+ *
1321
+ * Used by template editors to render page thumbnails on the client. The
1322
+ * matching `download_url` is also returned on each `template.pages[]` entry.
959
1323
  */
960
1324
  async downloadPage(templateId, pageId, accountId) {
961
1325
  const id = this.accountId(accountId);
@@ -979,7 +1343,7 @@ var TagResource = class extends BaseResource {
979
1343
  return this.call(
980
1344
  "Failed to list tags",
981
1345
  () => this.http.get(`/accounts/${id}/tags`, {
982
- params: cleanParams(params)
1346
+ params: cleanListParams(params)
983
1347
  })
984
1348
  );
985
1349
  }
@@ -1121,7 +1485,7 @@ var FieldsResource = class extends BaseResource {
1121
1485
  return this.call(
1122
1486
  "Failed to list field definitions",
1123
1487
  () => this.http.get(`/accounts/${id}/fields`, {
1124
- params: cleanParams(params)
1488
+ params: cleanListParams(params)
1125
1489
  })
1126
1490
  );
1127
1491
  }
@@ -1205,7 +1569,44 @@ var SignerDocumentsResource = class extends BaseResource {
1205
1569
  return this.callList(
1206
1570
  "Failed to list signer documents",
1207
1571
  () => this.http.get(`/signers/${sid}/documents`, {
1208
- params: { "signer-access-code": code, ...cleanParams(params) }
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 })
1209
1610
  })
1210
1611
  );
1211
1612
  }
@@ -1323,7 +1724,12 @@ var SignerDocumentsResource = class extends BaseResource {
1323
1724
  })
1324
1725
  );
1325
1726
  }
1326
- /** `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it. */
1727
+ /**
1728
+ * `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it.
1729
+ *
1730
+ * @param hasAcceptedTerms maps to the `has_accepted_terms` query param
1731
+ * (server default `false`); pass `true` once the signer has accepted terms.
1732
+ */
1327
1733
  async getAssignment(signerAccessCode, hasAcceptedTerms) {
1328
1734
  const code = this.requireId(signerAccessCode, "signer-access-code");
1329
1735
  return this.call(
@@ -1353,8 +1759,8 @@ var SignerDocumentsResource = class extends BaseResource {
1353
1759
  }
1354
1760
  /**
1355
1761
  * `PUT /documents/{documentId}/assignments/{assignmentId}/reject?signer-access-code=…`
1356
- * — signer-side decline. (Distinct from `assignments.cancel`, which is the
1357
- * workspace-side cancellation flow.)
1762
+ * — signer-side decline. (The workspace-side equivalent is to delete the
1763
+ * document via `documents.delete`; there is no workspace "cancel" endpoint.)
1358
1764
  */
1359
1765
  async decline(documentId, assignmentId, signerAccessCode, declineReason) {
1360
1766
  const did = this.requireId(documentId, "Document ID");
@@ -1420,6 +1826,21 @@ var WebhookVerifier = class {
1420
1826
  // src/client.ts
1421
1827
  var DEFAULT_BASE_URL = "https://api.assinafy.com.br/v1";
1422
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;
1423
1844
  constructor(options) {
1424
1845
  if (!options.apiKey && !options.token) {
1425
1846
  throw new ValidationError(
@@ -1445,6 +1866,10 @@ var AssinafyClient = class _AssinafyClient {
1445
1866
  timeout: options.timeout ?? 3e4,
1446
1867
  headers
1447
1868
  });
1869
+ const maxRetries = options.maxRetries ?? 2;
1870
+ if (maxRetries > 0) {
1871
+ installRateLimitRetry(this.axiosInstance, maxRetries, this.logger);
1872
+ }
1448
1873
  this.documents = new DocumentResource(this.axiosInstance, this.defaultAccountId, this.logger);
1449
1874
  this.signers = new SignerResource(this.axiosInstance, this.defaultAccountId, this.logger);
1450
1875
  this.workspaces = new WorkspaceResource(this.axiosInstance, void 0, this.logger);
@@ -1483,6 +1908,7 @@ var AssinafyClient = class _AssinafyClient {
1483
1908
  if (baseUrl !== void 0) opts.baseUrl = baseUrl;
1484
1909
  if (webhookSecret !== void 0) opts.webhookSecret = webhookSecret;
1485
1910
  if (config.timeout !== void 0) opts.timeout = config.timeout;
1911
+ if (config.maxRetries !== void 0) opts.maxRetries = config.maxRetries;
1486
1912
  if (config.logger !== void 0) opts.logger = config.logger;
1487
1913
  return new _AssinafyClient(opts);
1488
1914
  }
@@ -1542,6 +1968,27 @@ var AssinafyClient = class _AssinafyClient {
1542
1968
  function normaliseBaseUrl(raw) {
1543
1969
  return raw.endsWith("/") ? raw.slice(0, -1) : raw;
1544
1970
  }
1971
+ function installRateLimitRetry(http, maxRetries, logger) {
1972
+ http.interceptors.response.use(
1973
+ (response) => response,
1974
+ async (error) => {
1975
+ if (!import_axios2.default.isAxiosError(error) || error.response?.status !== 429 || !error.config) {
1976
+ throw error;
1977
+ }
1978
+ const config = error.config;
1979
+ const attempt = (config._retryCount ?? 0) + 1;
1980
+ if (attempt > maxRetries) throw error;
1981
+ config._retryCount = attempt;
1982
+ const delayMs = nextRetryDelayMs(
1983
+ error.response.headers,
1984
+ attempt
1985
+ );
1986
+ logger.warn("Rate limited (429); retrying after delay", { attempt, maxRetries, delayMs });
1987
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1988
+ return http(config);
1989
+ }
1990
+ );
1991
+ }
1545
1992
  // Annotate the CommonJS export names for ESM import in node:
1546
1993
  0 && (module.exports = {
1547
1994
  ApiError,
@@ -1549,8 +1996,10 @@ function normaliseBaseUrl(raw) {
1549
1996
  AssinafyClient,
1550
1997
  AssinafyError,
1551
1998
  AuthenticationResource,
1999
+ DEFAULT_WEBHOOK_EVENTS,
1552
2000
  DocumentResource,
1553
2001
  FieldsResource,
2002
+ MAX_UPLOAD_BYTES,
1554
2003
  NetworkError,
1555
2004
  SignerDocumentsResource,
1556
2005
  SignerResource,