@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.mjs CHANGED
@@ -87,9 +87,40 @@ function cleanParams(params) {
87
87
  return out;
88
88
  }
89
89
 
90
- // src/resources/documents.ts
91
- import { promises as fs } from "fs";
92
- import path from "path";
90
+ // src/support/retry.ts
91
+ function header(headers, name) {
92
+ if (!headers) return void 0;
93
+ const lower = name.toLowerCase();
94
+ for (const [key, value] of Object.entries(headers)) {
95
+ if (key.toLowerCase() === lower && value != null) {
96
+ return Array.isArray(value) ? String(value[0]) : String(value);
97
+ }
98
+ }
99
+ return void 0;
100
+ }
101
+ function retryDelayFromHeaders(headers) {
102
+ const retryAfter = header(headers, "retry-after");
103
+ if (retryAfter !== void 0) {
104
+ const seconds = Number(retryAfter);
105
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
106
+ const date = Date.parse(retryAfter);
107
+ if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
108
+ }
109
+ const reset = header(headers, "x-rate-limit-reset");
110
+ if (reset !== void 0) {
111
+ const seconds = Number(reset);
112
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1e3);
113
+ }
114
+ return void 0;
115
+ }
116
+ function backoffMs(attempt) {
117
+ return Math.min(1e3 * 2 ** Math.max(0, attempt - 1), 8e3);
118
+ }
119
+ function nextRetryDelayMs(headers, attempt, maxDelayMs = 3e4) {
120
+ const hinted = retryDelayFromHeaders(headers);
121
+ if (hinted !== void 0) return Math.min(hinted, maxDelayMs);
122
+ return Math.min(backoffMs(attempt), maxDelayMs);
123
+ }
93
124
 
94
125
  // src/resources/base.ts
95
126
  var BaseResource = class {
@@ -194,8 +225,49 @@ function toInt(value) {
194
225
  return Number.isFinite(n) ? n : void 0;
195
226
  }
196
227
 
197
- // src/resources/documents.ts
228
+ // src/resources/upload.ts
229
+ import { promises as fs } from "fs";
230
+ import path from "path";
198
231
  var MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
232
+ async function loadSource(source) {
233
+ if ("buffer" in source) {
234
+ if (!source.fileName) {
235
+ throw new ValidationError("fileName is required when uploading a Buffer");
236
+ }
237
+ return { buffer: source.buffer, fileName: source.fileName };
238
+ }
239
+ if (!source.filePath) {
240
+ throw new ValidationError("filePath is required");
241
+ }
242
+ const buffer = await fs.readFile(source.filePath);
243
+ return { buffer, fileName: source.fileName ?? path.basename(source.filePath) };
244
+ }
245
+ function validateUpload(buffer, fileName) {
246
+ if (!buffer || buffer.byteLength === 0) {
247
+ throw new ValidationError("File buffer is empty", { fileName });
248
+ }
249
+ if (!fileName.toLowerCase().endsWith(".pdf")) {
250
+ throw new ValidationError("Only PDF files are supported", { fileName });
251
+ }
252
+ if (buffer.byteLength > MAX_UPLOAD_BYTES) {
253
+ throw new ValidationError("File size exceeds maximum allowed (25MB)", {
254
+ fileSize: buffer.byteLength,
255
+ maxSize: MAX_UPLOAD_BYTES
256
+ });
257
+ }
258
+ }
259
+ function buildUploadForm(buffer, fileName, options = {}) {
260
+ const form = new FormData();
261
+ const view = new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
262
+ form.append("file", new Blob([view], { type: "application/pdf" }), fileName);
263
+ form.append("name", options.name ?? fileName);
264
+ if (options.metadata) {
265
+ form.append("metadata", JSON.stringify(options.metadata));
266
+ }
267
+ return form;
268
+ }
269
+
270
+ // src/resources/documents.ts
199
271
  var READY_STATUSES = /* @__PURE__ */ new Set([
200
272
  "metadata_ready",
201
273
  "pending_signature",
@@ -221,7 +293,9 @@ var DocumentResource = class extends BaseResource {
221
293
  const { buffer, fileName } = await loadSource(source);
222
294
  validateUpload(buffer, fileName);
223
295
  const accountId = this.accountId(options.accountId);
224
- const form = buildUploadForm(buffer, fileName, options.metadata);
296
+ const formOptions = {};
297
+ if (options.metadata !== void 0) formOptions.metadata = options.metadata;
298
+ const form = buildUploadForm(buffer, fileName, formOptions);
225
299
  this.logger.info("Uploading document", { fileName, size: buffer.byteLength });
226
300
  const document = await this.call(
227
301
  "Document upload failed",
@@ -397,7 +471,12 @@ var DocumentResource = class extends BaseResource {
397
471
  () => this.http.post(`/accounts/${accId}/templates/${tmplId}/documents`, body)
398
472
  );
399
473
  }
400
- /** Estimate the credit cost of creating a document from a template. */
474
+ /**
475
+ * Estimate the credit cost of creating a document from a template.
476
+ *
477
+ * @returns an {@link ICostEstimate}: `total_credits`, balances, and a
478
+ * per-line `breakdown` of what the operation would consume.
479
+ */
401
480
  async estimateCostFromTemplate(templateId, signers, accountId) {
402
481
  const tmplId = this.requireId(templateId, "Template ID");
403
482
  const accId = this.accountId(accountId);
@@ -466,43 +545,6 @@ var DocumentResource = class extends BaseResource {
466
545
  return { signed, total, pending, percentage };
467
546
  }
468
547
  };
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
548
  function sleep(ms) {
507
549
  return new Promise((resolve) => setTimeout(resolve, ms));
508
550
  }
@@ -718,7 +760,15 @@ var AssignmentResource = class extends BaseResource {
718
760
  () => this.http.post(`/documents/${docId}/assignments`, body)
719
761
  );
720
762
  }
721
- /** Estimate the cost (in credits) of creating the assignment. */
763
+ /**
764
+ * Estimate the cost (in credits/documents) of creating the assignment.
765
+ *
766
+ * Signer entries may omit `id` and supply only `verification_method` /
767
+ * `notification_methods` when only the channel mix matters for the estimate.
768
+ *
769
+ * @returns an {@link ICostEstimate} with `total_credits`, balances, and a
770
+ * line-item `breakdown`.
771
+ */
722
772
  async estimateCost(documentId, payload) {
723
773
  const docId = this.requireId(documentId, "Document ID");
724
774
  return this.call(
@@ -753,7 +803,11 @@ var AssignmentResource = class extends BaseResource {
753
803
  () => this.http.put(`/documents/${docId}/assignments/${asgId}/signers/${sid}/resend`)
754
804
  );
755
805
  }
756
- /** Estimate the cost of resending a signer notification. */
806
+ /**
807
+ * Estimate the cost of resending a signer notification.
808
+ *
809
+ * @returns an {@link IResendCostEstimate} (`total`, `breakdown`, balances).
810
+ */
757
811
  async estimateResendCost(documentId, assignmentId, signerId) {
758
812
  const docId = this.requireId(documentId, "Document ID");
759
813
  const asgId = this.requireId(assignmentId, "Assignment ID");
@@ -777,26 +831,10 @@ var AssignmentResource = class extends BaseResource {
777
831
  () => this.http.get(`/documents/${docId}/assignments/${asgId}/whatsapp-notifications`)
778
832
  );
779
833
  }
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
834
  };
797
835
 
798
836
  // src/resources/webhooks.ts
799
- var DEFAULT_EVENTS = [
837
+ var DEFAULT_WEBHOOK_EVENTS = [
800
838
  "document_ready",
801
839
  "document_prepared",
802
840
  "signer_signed_document",
@@ -804,7 +842,21 @@ var DEFAULT_EVENTS = [
804
842
  "document_processing_failed"
805
843
  ];
806
844
  var WebhookResource = class extends BaseResource {
807
- /** Register (or replace) the webhook subscription for the workspace. */
845
+ /**
846
+ * Register (or replace) the workspace's single webhook subscription
847
+ * (`PUT /accounts/{id}/webhooks/subscriptions`). There is exactly one
848
+ * subscription per workspace, keyed by URL.
849
+ *
850
+ * When `events` is omitted or empty, {@link DEFAULT_WEBHOOK_EVENTS} is used
851
+ * (`document_ready`, `document_prepared`, `signer_signed_document`,
852
+ * `signer_rejected_document`, `document_processing_failed`).
853
+ *
854
+ * @example
855
+ * ```ts
856
+ * await client.webhooks.register({ url: 'https://example.com/hook', email: 'ops@example.com' });
857
+ * // → { url, email, events: [...], is_active: true, updated_at: '2026-…' }
858
+ * ```
859
+ */
808
860
  async register(payload, accountId) {
809
861
  if (!payload.url) throw new ValidationError("Webhook URL is required");
810
862
  if (!payload.email) throw new ValidationError("Webhook email is required");
@@ -812,7 +864,7 @@ var WebhookResource = class extends BaseResource {
812
864
  const body = {
813
865
  url: payload.url,
814
866
  email: payload.email,
815
- events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_EVENTS,
867
+ events: payload.events && payload.events.length > 0 ? payload.events : DEFAULT_WEBHOOK_EVENTS,
816
868
  is_active: payload.is_active ?? true
817
869
  };
818
870
  this.logger.info("Registering webhook", { url: payload.url });
@@ -877,6 +929,46 @@ var WebhookResource = class extends BaseResource {
877
929
 
878
930
  // src/resources/templates.ts
879
931
  var TemplateResource = class extends BaseResource {
932
+ /**
933
+ * Create a template by uploading a PDF (`POST /accounts/{id}/templates`).
934
+ *
935
+ * The template is created in `Uploaded` status and transitions to `Ready`
936
+ * once the platform finishes processing its pages. Configure roles/fields
937
+ * afterwards in the Assinafy editor.
938
+ *
939
+ * @example
940
+ * ```ts
941
+ * const tmpl = await client.templates.create(
942
+ * { filePath: './nda.pdf' },
943
+ * { name: 'NDA template' },
944
+ * );
945
+ * // → { resource: 'template', id, name, status: 'Uploaded',
946
+ * // roles: [{ id, name: 'TemplateEditor', assignment_type: 'Editor' }],
947
+ * // pages: [], tags: [], created_at, updated_at }
948
+ * ```
949
+ */
950
+ async create(source, options = {}) {
951
+ const { buffer, fileName } = await loadSource(source);
952
+ validateUpload(buffer, fileName);
953
+ const id = this.accountId(options.accountId);
954
+ const formOptions = {};
955
+ if (options.name !== void 0) formOptions.name = options.name;
956
+ const form = buildUploadForm(buffer, fileName, formOptions);
957
+ this.logger.info("Creating template", { fileName, size: buffer.byteLength });
958
+ const template = await this.call(
959
+ "Failed to create template",
960
+ () => this.http.post(`/accounts/${id}/templates`, form, {
961
+ headers: { "Content-Type": "multipart/form-data" }
962
+ })
963
+ );
964
+ if (!template?.id) {
965
+ throw new ValidationError("Template upload succeeded but no template ID was returned", {
966
+ response: template
967
+ });
968
+ }
969
+ this.logger.info("Template created", { templateId: template.id });
970
+ return template;
971
+ }
880
972
  /** List templates for the workspace. */
881
973
  async list(params = {}, accountId) {
882
974
  const id = this.accountId(accountId);
@@ -886,11 +978,10 @@ var TemplateResource = class extends BaseResource {
886
978
  );
887
979
  }
888
980
  /**
889
- * Get a template by ID.
981
+ * Get a template by ID (`GET /accounts/{id}/templates/{template_id}`).
890
982
  *
891
- * Note: the swagger only documents the list endpoint; this single-resource
892
- * `GET /accounts/{id}/templates/{id}` is exposed by the platform and used
893
- * by the official PHP SDK.
983
+ * Unlike the list endpoint, the single-template response includes `pages`
984
+ * (with per-page `download_url`) and `default_document_tags`.
894
985
  */
895
986
  async get(templateId, accountId) {
896
987
  const id = this.accountId(accountId);
@@ -901,9 +992,40 @@ var TemplateResource = class extends BaseResource {
901
992
  );
902
993
  }
903
994
  /**
904
- * `GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`
905
- * download a template page as a JPEG (used by template editors to render
906
- * thumbnails on the client).
995
+ * Update a template's `name` and/or default `message`
996
+ * (`PUT /accounts/{id}/templates/{template_id}`). Returns the updated template.
997
+ *
998
+ * @example
999
+ * ```ts
1000
+ * await client.templates.update(templateId, { name: 'NDA v2', message: 'Please sign' });
1001
+ * ```
1002
+ */
1003
+ async update(templateId, payload, accountId) {
1004
+ const id = this.accountId(accountId);
1005
+ const tmplId = this.requireId(templateId, "Template ID");
1006
+ return this.call(
1007
+ "Failed to update template",
1008
+ () => this.http.put(
1009
+ `/accounts/${id}/templates/${tmplId}`,
1010
+ cleanParams(payload)
1011
+ )
1012
+ );
1013
+ }
1014
+ /** Delete a template (`DELETE /accounts/{id}/templates/{template_id}`). */
1015
+ async delete(templateId, accountId) {
1016
+ const id = this.accountId(accountId);
1017
+ const tmplId = this.requireId(templateId, "Template ID");
1018
+ return this.callVoid(
1019
+ "Failed to delete template",
1020
+ () => this.http.delete(`/accounts/${id}/templates/${tmplId}`)
1021
+ );
1022
+ }
1023
+ /**
1024
+ * Download a template page as a JPEG
1025
+ * (`GET /accounts/{id}/templates/{template_id}/pages/{page_id}/download`).
1026
+ *
1027
+ * Used by template editors to render page thumbnails on the client. The
1028
+ * matching `download_url` is also returned on each `template.pages[]` entry.
907
1029
  */
908
1030
  async downloadPage(templateId, pageId, accountId) {
909
1031
  const id = this.accountId(accountId);
@@ -1271,7 +1393,12 @@ var SignerDocumentsResource = class extends BaseResource {
1271
1393
  })
1272
1394
  );
1273
1395
  }
1274
- /** `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it. */
1396
+ /**
1397
+ * `GET /sign?signer-access-code=…` — fetch the assignment as the signer sees it.
1398
+ *
1399
+ * @param hasAcceptedTerms maps to the `has_accepted_terms` query param
1400
+ * (server default `false`); pass `true` once the signer has accepted terms.
1401
+ */
1275
1402
  async getAssignment(signerAccessCode, hasAcceptedTerms) {
1276
1403
  const code = this.requireId(signerAccessCode, "signer-access-code");
1277
1404
  return this.call(
@@ -1301,8 +1428,8 @@ var SignerDocumentsResource = class extends BaseResource {
1301
1428
  }
1302
1429
  /**
1303
1430
  * `PUT /documents/{documentId}/assignments/{assignmentId}/reject?signer-access-code=…`
1304
- * — signer-side decline. (Distinct from `assignments.cancel`, which is the
1305
- * workspace-side cancellation flow.)
1431
+ * — signer-side decline. (The workspace-side equivalent is to delete the
1432
+ * document via `documents.delete`; there is no workspace "cancel" endpoint.)
1306
1433
  */
1307
1434
  async decline(documentId, assignmentId, signerAccessCode, declineReason) {
1308
1435
  const did = this.requireId(documentId, "Document ID");
@@ -1393,6 +1520,10 @@ var AssinafyClient = class _AssinafyClient {
1393
1520
  timeout: options.timeout ?? 3e4,
1394
1521
  headers
1395
1522
  });
1523
+ const maxRetries = options.maxRetries ?? 2;
1524
+ if (maxRetries > 0) {
1525
+ installRateLimitRetry(this.axiosInstance, maxRetries, this.logger);
1526
+ }
1396
1527
  this.documents = new DocumentResource(this.axiosInstance, this.defaultAccountId, this.logger);
1397
1528
  this.signers = new SignerResource(this.axiosInstance, this.defaultAccountId, this.logger);
1398
1529
  this.workspaces = new WorkspaceResource(this.axiosInstance, void 0, this.logger);
@@ -1431,6 +1562,7 @@ var AssinafyClient = class _AssinafyClient {
1431
1562
  if (baseUrl !== void 0) opts.baseUrl = baseUrl;
1432
1563
  if (webhookSecret !== void 0) opts.webhookSecret = webhookSecret;
1433
1564
  if (config.timeout !== void 0) opts.timeout = config.timeout;
1565
+ if (config.maxRetries !== void 0) opts.maxRetries = config.maxRetries;
1434
1566
  if (config.logger !== void 0) opts.logger = config.logger;
1435
1567
  return new _AssinafyClient(opts);
1436
1568
  }
@@ -1490,12 +1622,34 @@ var AssinafyClient = class _AssinafyClient {
1490
1622
  function normaliseBaseUrl(raw) {
1491
1623
  return raw.endsWith("/") ? raw.slice(0, -1) : raw;
1492
1624
  }
1625
+ function installRateLimitRetry(http, maxRetries, logger) {
1626
+ http.interceptors.response.use(
1627
+ (response) => response,
1628
+ async (error) => {
1629
+ if (!axios2.isAxiosError(error) || error.response?.status !== 429 || !error.config) {
1630
+ throw error;
1631
+ }
1632
+ const config = error.config;
1633
+ const attempt = (config._retryCount ?? 0) + 1;
1634
+ if (attempt > maxRetries) throw error;
1635
+ config._retryCount = attempt;
1636
+ const delayMs = nextRetryDelayMs(
1637
+ error.response.headers,
1638
+ attempt
1639
+ );
1640
+ logger.warn("Rate limited (429); retrying after delay", { attempt, maxRetries, delayMs });
1641
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1642
+ return http(config);
1643
+ }
1644
+ );
1645
+ }
1493
1646
  export {
1494
1647
  ApiError,
1495
1648
  AssignmentResource,
1496
1649
  AssinafyClient,
1497
1650
  AssinafyError,
1498
1651
  AuthenticationResource,
1652
+ DEFAULT_WEBHOOK_EVENTS,
1499
1653
  DocumentResource,
1500
1654
  FieldsResource,
1501
1655
  NetworkError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assinafy/sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "TypeScript SDK for Assinafy API - Digital signature platform",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -15,8 +15,8 @@
15
15
  "scripts": {
16
16
  "build": "tsup src/index.ts --format cjs,esm --dts --clean",
17
17
  "test": "bun test",
18
- "lint": "eslint src --ext .ts",
19
- "lint:fix": "eslint src --ext .ts --fix",
18
+ "lint": "eslint src",
19
+ "lint:fix": "eslint src --fix",
20
20
  "prepublishOnly": "bun run build",
21
21
  "release": "bun run typecheck && bun run lint && bun test && bun run build && npm publish --access public && npm publish --registry=https://npm.pkg.github.com",
22
22
  "typecheck": "tsc --noEmit"
@@ -40,7 +40,7 @@
40
40
  "author": "Assinafy API Team",
41
41
  "license": "MIT",
42
42
  "engines": {
43
- "node": ">=18"
43
+ "node": ">=20"
44
44
  },
45
45
  "repository": {
46
46
  "type": "git",
@@ -51,16 +51,19 @@
51
51
  },
52
52
  "homepage": "https://github.com/assinafy/typescript-sdk/#readme",
53
53
  "devDependencies": {
54
+ "@eslint/js": "^9.0.0",
54
55
  "@types/bun": "^1.3.0",
55
- "@typescript-eslint/eslint-plugin": "^6.0.0",
56
- "@typescript-eslint/parser": "^6.0.0",
57
- "eslint": "^8.0.0",
56
+ "eslint": "^9.0.0",
58
57
  "tsup": "^8.0.0",
59
- "typescript": "^5.0.0"
58
+ "typescript": "^5.0.0",
59
+ "typescript-eslint": "^8.0.0"
60
60
  },
61
61
  "dependencies": {
62
62
  "axios": "^1.6.0"
63
63
  },
64
+ "overrides": {
65
+ "esbuild": "0.27.0"
66
+ },
64
67
  "files": [
65
68
  "dist",
66
69
  "README.md",