@assinafy/sdk 1.4.0 → 1.5.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,6 +35,7 @@ __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,
40
41
  NetworkError: () => NetworkError,
@@ -139,9 +140,40 @@ function cleanParams(params) {
139
140
  return out;
140
141
  }
141
142
 
142
- // src/resources/documents.ts
143
- var import_node_fs = require("fs");
144
- var import_node_path = __toESM(require("path"));
143
+ // src/support/retry.ts
144
+ function header(headers, name) {
145
+ if (!headers) return void 0;
146
+ const lower = name.toLowerCase();
147
+ for (const [key, value] of Object.entries(headers)) {
148
+ if (key.toLowerCase() === lower && value != null) {
149
+ return Array.isArray(value) ? String(value[0]) : String(value);
150
+ }
151
+ }
152
+ return void 0;
153
+ }
154
+ function retryDelayFromHeaders(headers) {
155
+ const retryAfter = header(headers, "retry-after");
156
+ if (retryAfter !== void 0) {
157
+ const seconds = Number(retryAfter);
158
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
159
+ const date = Date.parse(retryAfter);
160
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
161
+ }
162
+ const reset = header(headers, "x-rate-limit-reset");
163
+ if (reset !== void 0) {
164
+ const seconds = Number(reset);
165
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
166
+ }
167
+ return void 0;
168
+ }
169
+ function backoffMs(attempt) {
170
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 8e3);
171
+ }
172
+ function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
173
+ const hinted = retryDelayFromHeaders(headers);
174
+ if (hinted !== void 0) return Math.min(hinted, maxDelayMs);
175
+ return Math.min(backoffMs(attempt), maxDelayMs);
176
+ }
145
177
 
146
178
  // src/resources/base.ts
147
179
  var BaseResource = class {
@@ -246,8 +278,49 @@ function toInt(value) {
246
278
  return Number.isFinite(n) ? n : void 0;
247
279
  }
248
280
 
249
- // src/resources/documents.ts
281
+ // src/resources/upload.ts
282
+ var import_node_fs = require("fs");
283
+ var import_node_path = __toESM(require("path"));
250
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
+ // src/resources/documents.ts
251
324
  var READY_STATUSES = /* @__PURE__ */ new Set([
252
325
  "metadata_ready",
253
326
  "pending_signature",
@@ -273,7 +346,9 @@ var DocumentResource = class extends BaseResource {
273
346
  const { buffer, fileName } = await loadSource(source);
274
347
  validateUpload(buffer, fileName);
275
348
  const accountId = this.accountId(options.accountId);
276
- const form = buildUploadForm(buffer, fileName, options.metadata);
349
+ const formOptions = {};
350
+ if (options.metadata !== void 0) formOptions.metadata = options.metadata;
351
+ const form = buildUploadForm(buffer, fileName, formOptions);
277
352
  this.logger.info("Uploading document", { fileName, size: buffer.byteLength });
278
353
  const document = await this.call(
279
354
  "Document upload failed",
@@ -449,7 +524,12 @@ var DocumentResource = class extends BaseResource {
449
524
  () => this.http.post(`/accounts/${accId}/templates/${tmplId}/documents`, body)
450
525
  );
451
526
  }
452
- /** Estimate the credit cost of creating a document from a template. */
527
+ /**
528
+ * Estimate the credit cost of creating a document from a template.
529
+ *
530
+ * @returns an {@link ICostEstimate}: `total_credits`, balances, and a
531
+ * per-line `breakdown` of what the operation would consume.
532
+ */
453
533
  async estimateCostFromTemplate(templateId, signers, accountId) {
454
534
  const tmplId = this.requireId(templateId, "Template ID");
455
535
  const accId = this.accountId(accountId);
@@ -518,43 +598,6 @@ var DocumentResource = class extends BaseResource {
518
598
  return { signed, total, pending, percentage };
519
599
  }
520
600
  };
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
601
  function sleep(ms) {
559
602
  return new Promise((resolve) => setTimeout(resolve, ms));
560
603
  }
@@ -770,7 +813,15 @@ var AssignmentResource = class extends BaseResource {
770
813
  () => this.http.post(`/documents/${docId}/assignments`, body)
771
814
  );
772
815
  }
773
- /** Estimate the cost (in credits) of creating the assignment. */
816
+ /**
817
+ * Estimate the cost (in credits/documents) of creating the assignment.
818
+ *
819
+ * Signer entries may omit `id` and supply only `verification_method` /
820
+ * `notification_methods` when only the channel mix matters for the estimate.
821
+ *
822
+ * @returns an {@link ICostEstimate} with `total_credits`, balances, and a
823
+ * line-item `breakdown`.
824
+ */
774
825
  async estimateCost(documentId, payload) {
775
826
  const docId = this.requireId(documentId, "Document ID");
776
827
  return this.call(
@@ -805,7 +856,11 @@ var AssignmentResource = class extends BaseResource {
805
856
  () => this.http.put(`/documents/${docId}/assignments/${asgId}/signers/${sid}/resend`)
806
857
  );
807
858
  }
808
- /** Estimate the cost of resending a signer notification. */
859
+ /**
860
+ * Estimate the cost of resending a signer notification.
861
+ *
862
+ * @returns an {@link IResendCostEstimate} (`total`, `breakdown`, balances).
863
+ */
809
864
  async estimateResendCost(documentId, assignmentId, signerId) {
810
865
  const docId = this.requireId(documentId, "Document ID");
811
866
  const asgId = this.requireId(assignmentId, "Assignment ID");
@@ -829,26 +884,10 @@ var AssignmentResource = class extends BaseResource {
829
884
  () => this.http.get(`/documents/${docId}/assignments/${asgId}/whatsapp-notifications`)
830
885
  );
831
886
  }
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
887
  };
849
888
 
850
889
  // src/resources/webhooks.ts
851
- var DEFAULT_EVENTS = [
890
+ var DEFAULT_WEBHOOK_EVENTS = [
852
891
  "document_ready",
853
892
  "document_prepared",
854
893
  "signer_signed_document",
@@ -856,7 +895,21 @@ var DEFAULT_EVENTS = [
856
895
  "document_processing_failed"
857
896
  ];
858
897
  var WebhookResource = class extends BaseResource {
859
- /** Register (or replace) the webhook subscription for the workspace. */
898
+ /**
899
+ * Register (or replace) the workspace's single webhook subscription
900
+ * (`PUT /accounts/{id}/webhooks/subscriptions`). There is exactly one
901
+ * subscription per workspace, keyed by URL.
902
+ *
903
+ * When `events` is omitted or empty, {@link DEFAULT_WEBHOOK_EVENTS} is used
904
+ * (`document_ready`, `document_prepared`, `signer_signed_document`,
905
+ * `signer_rejected_document`, `document_processing_failed`).
906
+ *
907
+ * @example
908
+ * ```ts
909
+ * await client.webhooks.register({ url: 'https://example.com/hook', email: 'ops@example.com' });
910
+ * // → { url, email, events: [...], is_active: true, updated_at: '2026-…' }
911
+ * ```
912
+ */
860
913
  async register(payload, accountId) {
861
914
  if (!payload.url) throw new ValidationError("Webhook URL is required");
862
915
  if (!payload.email) throw new ValidationError("Webhook email is required");
@@ -864,7 +917,7 @@ var WebhookResource = class extends BaseResource {
864
917
  const body = {
865
918
  url: payload.url,
866
919
  email: payload.email,
867
- events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_EVENTS,
920
+ events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_WEBHOOK_EVENTS,
868
921
  is_active: payload.is_active ?? true
869
922
  };
870
923
  this.logger.info("Registering webhook", { url: payload.url });
@@ -929,6 +982,46 @@ var WebhookResource = class extends BaseResource {
929
982
 
930
983
  // src/resources/templates.ts
931
984
  var TemplateResource = class extends BaseResource {
985
+ /**
986
+ * Create a template by uploading a PDF (`POST /accounts/{id}/templates`).
987
+ *
988
+ * The template is created in `Uploaded` status and transitions to `Ready`
989
+ * once the platform finishes processing its pages. Configure roles/fields
990
+ * afterwards in the Assinafy editor.
991
+ *
992
+ * @example
993
+ * ```ts
994
+ * const tmpl = await client.templates.create(
995
+ * { filePath: './nda.pdf' },
996
+ * { name: 'NDA template' },
997
+ * );
998
+ * // → { resource: 'template', id, name, status: 'Uploaded',
999
+ * // roles: [{ id, name: 'TemplateEditor', assignment_type: 'Editor' }],
1000
+ * // pages: [], tags: [], created_at, updated_at }
1001
+ * ```
1002
+ */
1003
+ async create(source, options = {}) {
1004
+ const { buffer, fileName } = await loadSource(source);
1005
+ validateUpload(buffer, fileName);
1006
+ const id = this.accountId(options.accountId);
1007
+ const formOptions = {};
1008
+ if (options.name !== void 0) formOptions.name = options.name;
1009
+ const form = buildUploadForm(buffer, fileName, formOptions);
1010
+ this.logger.info("Creating template", { fileName, size: buffer.byteLength });
1011
+ const template = await this.call(
1012
+ "Failed to create template",
1013
+ () => this.http.post(`/accounts/${id}/templates`, form, {
1014
+ headers: { "Content-Type": "multipart/form-data" }
1015
+ })
1016
+ );
1017
+ if (!template?.id) {
1018
+ throw new ValidationError("Template upload succeeded but no template ID was returned", {
1019
+ response: template
1020
+ });
1021
+ }
1022
+ this.logger.info("Template created", { templateId: template.id });
1023
+ return template;
1024
+ }
932
1025
  /** List templates for the workspace. */
933
1026
  async list(params = {}, accountId) {
934
1027
  const id = this.accountId(accountId);
@@ -938,11 +1031,10 @@ var TemplateResource = class extends BaseResource {
938
1031
  );
939
1032
  }
940
1033
  /**
941
- * Get a template by ID.
1034
+ * Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
942
1035
  *
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.
1036
+ * Unlike the list endpoint, the single-template response includes `pages`
1037
+ * (with per-page `download_url`) and `default_document_tags`.
946
1038
  */
947
1039
  async get(templateId, accountId) {
948
1040
  const id = this.accountId(accountId);
@@ -953,9 +1045,40 @@ var TemplateResource = class extends BaseResource {
953
1045
  );
954
1046
  }
955
1047
  /**
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).
1048
+ * Update a template's `name` and/or default `message`
1049
+ * (`PUT /accounts/{id}/templates/{template_id}`). Returns the updated template.
1050
+ *
1051
+ * @example
1052
+ * ```ts
1053
+ * await client.templates.update(templateId, { name: 'NDA v2', message: 'Please sign' });
1054
+ * ```
1055
+ */
1056
+ async update(templateId, payload, accountId) {
1057
+ const id = this.accountId(accountId);
1058
+ const tmplId = this.requireId(templateId, "Template ID");
1059
+ return this.call(
1060
+ "Failed to update template",
1061
+ () => this.http.put(
1062
+ `/accounts/${id}/templates/${tmplId}`,
1063
+ cleanParams(payload)
1064
+ )
1065
+ );
1066
+ }
1067
+ /** Delete a template (`DELETE /accounts/{id}/templates/{template_id}`). */
1068
+ async delete(templateId, accountId) {
1069
+ const id = this.accountId(accountId);
1070
+ const tmplId = this.requireId(templateId, "Template ID");
1071
+ return this.callVoid(
1072
+ "Failed to delete template",
1073
+ () => this.http.delete(`/accounts/${id}/templates/${tmplId}`)
1074
+ );
1075
+ }
1076
+ /**
1077
+ * Download a template page as a JPEG
1078
+ * (`GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`).
1079
+ *
1080
+ * Used by template editors to render page thumbnails on the client. The
1081
+ * matching `download_url` is also returned on each `template.pages[]` entry.
959
1082
  */
960
1083
  async downloadPage(templateId, pageId, accountId) {
961
1084
  const id = this.accountId(accountId);
@@ -1323,7 +1446,12 @@ var SignerDocumentsResource = class extends BaseResource {
1323
1446
  })
1324
1447
  );
1325
1448
  }
1326
- /** `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it. */
1449
+ /**
1450
+ * `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it.
1451
+ *
1452
+ * @param hasAcceptedTerms maps to the `has_accepted_terms` query param
1453
+ * (server default `false`); pass `true` once the signer has accepted terms.
1454
+ */
1327
1455
  async getAssignment(signerAccessCode, hasAcceptedTerms) {
1328
1456
  const code = this.requireId(signerAccessCode, "signer-access-code");
1329
1457
  return this.call(
@@ -1353,8 +1481,8 @@ var SignerDocumentsResource = class extends BaseResource {
1353
1481
  }
1354
1482
  /**
1355
1483
  * `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.)
1484
+ * — signer-side decline. (The workspace-side equivalent is to delete the
1485
+ * document via `documents.delete`; there is no workspace "cancel" endpoint.)
1358
1486
  */
1359
1487
  async decline(documentId, assignmentId, signerAccessCode, declineReason) {
1360
1488
  const did = this.requireId(documentId, "Document ID");
@@ -1445,6 +1573,10 @@ var AssinafyClient = class _AssinafyClient {
1445
1573
  timeout: options.timeout ?? 3e4,
1446
1574
  headers
1447
1575
  });
1576
+ const maxRetries = options.maxRetries ?? 2;
1577
+ if (maxRetries > 0) {
1578
+ installRateLimitRetry(this.axiosInstance, maxRetries, this.logger);
1579
+ }
1448
1580
  this.documents = new DocumentResource(this.axiosInstance, this.defaultAccountId, this.logger);
1449
1581
  this.signers = new SignerResource(this.axiosInstance, this.defaultAccountId, this.logger);
1450
1582
  this.workspaces = new WorkspaceResource(this.axiosInstance, void 0, this.logger);
@@ -1483,6 +1615,7 @@ var AssinafyClient = class _AssinafyClient {
1483
1615
  if (baseUrl !== void 0) opts.baseUrl = baseUrl;
1484
1616
  if (webhookSecret !== void 0) opts.webhookSecret = webhookSecret;
1485
1617
  if (config.timeout !== void 0) opts.timeout = config.timeout;
1618
+ if (config.maxRetries !== void 0) opts.maxRetries = config.maxRetries;
1486
1619
  if (config.logger !== void 0) opts.logger = config.logger;
1487
1620
  return new _AssinafyClient(opts);
1488
1621
  }
@@ -1542,6 +1675,27 @@ var AssinafyClient = class _AssinafyClient {
1542
1675
  function normaliseBaseUrl(raw) {
1543
1676
  return raw.endsWith("/") ? raw.slice(0, -1) : raw;
1544
1677
  }
1678
+ function installRateLimitRetry(http, maxRetries, logger) {
1679
+ http.interceptors.response.use(
1680
+ (response) => response,
1681
+ async (error) => {
1682
+ if (!import_axios2.default.isAxiosError(error) || error.response?.status !== 429 || !error.config) {
1683
+ throw error;
1684
+ }
1685
+ const config = error.config;
1686
+ const attempt = (config._retryCount ?? 0) + 1;
1687
+ if (attempt > maxRetries) throw error;
1688
+ config._retryCount = attempt;
1689
+ const delayMs = nextRetryDelayMs(
1690
+ error.response.headers,
1691
+ attempt
1692
+ );
1693
+ logger.warn("Rate limited (429); retrying after delay", { attempt, maxRetries, delayMs });
1694
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1695
+ return http(config);
1696
+ }
1697
+ );
1698
+ }
1545
1699
  // Annotate the CommonJS export names for ESM import in node:
1546
1700
  0 && (module.exports = {
1547
1701
  ApiError,
@@ -1549,6 +1703,7 @@ function normaliseBaseUrl(raw) {
1549
1703
  AssinafyClient,
1550
1704
  AssinafyError,
1551
1705
  AuthenticationResource,
1706
+ DEFAULT_WEBHOOK_EVENTS,
1552
1707
  DocumentResource,
1553
1708
  FieldsResource,
1554
1709
  NetworkError,