@xenterprises/fastify-xsignwell 1.1.1 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xenterprises/fastify-xsignwell",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Fastify plugin for SignWell document signing API integration",
5
5
  "type": "module",
6
6
  "main": "src/xSignwell.js",
@@ -21,7 +21,7 @@
21
21
  "api"
22
22
  ],
23
23
  "author": "X Enterprises",
24
- "license": "MIT",
24
+ "license": "UNLICENSED",
25
25
  "dependencies": {
26
26
  "fastify-plugin": "^5.0.1"
27
27
  },
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared API request helper for SignWell services.
3
+ *
4
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance (for logging)
5
+ * @param {Object} config - Plugin configuration
6
+ * @param {string} endpoint - API endpoint path
7
+ * @param {Object} [options] - Fetch options
8
+ * @returns {Promise<Object>} Parsed API response
9
+ */
10
+ export async function apiRequest(fastify, config, endpoint, options = {}) {
11
+ const url = `${config.baseUrl}${endpoint}`;
12
+ const headers = {
13
+ "X-Api-Key": config.apiKey,
14
+ "Content-Type": "application/json",
15
+ ...options.headers,
16
+ };
17
+
18
+ let response;
19
+ try {
20
+ response = await fetch(url, { ...options, headers });
21
+ } catch (err) {
22
+ throw new Error(`[xSignwell] Network error calling ${endpoint}: ${err.message}`);
23
+ }
24
+
25
+ if (!response.ok) {
26
+ const errorText = await response.text();
27
+ let errorData;
28
+ try {
29
+ errorData = JSON.parse(errorText);
30
+ } catch {
31
+ errorData = { message: errorText };
32
+ }
33
+ fastify.log.error(
34
+ { endpoint, status: response.status, error: errorData },
35
+ "[xSignwell] API error"
36
+ );
37
+ const err = new Error(
38
+ `[xSignwell] ${errorData.message || `API error: ${response.status}`}`
39
+ );
40
+ err.statusCode = response.status;
41
+ err.signwellError = errorData;
42
+ throw err;
43
+ }
44
+
45
+ const text = await response.text();
46
+ if (!text) return { success: true };
47
+
48
+ return JSON.parse(text);
49
+ }
@@ -6,100 +6,67 @@
6
6
  * @see https://developers.signwell.com/reference/get_api-v1-bulk-sends-id
7
7
  */
8
8
 
9
+ import { apiRequest } from "../apiRequest.js";
10
+
9
11
  /**
10
- * Setup bulk send service
11
- *
12
- * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
- * @param {Object} config - Plugin configuration
12
+ * @param {import('fastify').FastifyInstance} fastify
13
+ * @param {Object} config
14
14
  */
15
15
  export async function setupBulkSend(fastify, config) {
16
- const { apiKey, baseUrl, testMode } = config;
17
-
18
- /**
19
- * Make an API request to SignWell
20
- * @param {string} endpoint - API endpoint
21
- * @param {Object} options - Fetch options
22
- * @returns {Promise<Object>} API response
23
- */
24
- async function apiRequest(endpoint, options = {}) {
25
- const url = `${baseUrl}${endpoint}`;
26
- const headers = {
27
- "X-Api-Key": apiKey,
28
- "Content-Type": "application/json",
29
- ...options.headers,
30
- };
31
-
32
- const response = await fetch(url, {
33
- ...options,
34
- headers,
35
- });
36
-
37
- if (!response.ok) {
38
- const errorText = await response.text();
39
- let errorData;
40
- try {
41
- errorData = JSON.parse(errorText);
42
- } catch {
43
- errorData = { message: errorText };
44
- }
45
- fastify.log.error({ endpoint, status: response.status, error: errorData }, "SignWell API error");
46
- throw new Error(errorData.message || `SignWell API error: ${response.status}`);
47
- }
48
-
49
- const text = await response.text();
50
- if (!text) return { success: true };
16
+ const { testMode } = config;
51
17
 
52
- return JSON.parse(text);
18
+ function req(endpoint, options) {
19
+ return apiRequest(fastify, config, endpoint, options);
53
20
  }
54
21
 
55
22
  /**
56
23
  * Get a bulk send by ID
57
- * @param {string} bulkSendId - Bulk send ID
58
- * @returns {Promise<Object>} Bulk send details
24
+ * @param {string} bulkSendId
25
+ * @returns {Promise<Object>}
59
26
  */
60
27
  async function get(bulkSendId) {
61
- return apiRequest(`/bulk_sends/${bulkSendId}`, {
62
- method: "GET",
63
- });
28
+ if (!bulkSendId || typeof bulkSendId !== "string") {
29
+ throw new Error("[xSignwell] bulkSend.get: bulkSendId (string) is required");
30
+ }
31
+ return req(`/bulk_sends/${bulkSendId}`, { method: "GET" });
64
32
  }
65
33
 
66
34
  /**
67
35
  * List all bulk sends
68
- * @param {Object} [params] - Query parameters
69
- * @param {number} [params.page] - Page number
70
- * @param {number} [params.limit] - Items per page
71
- * @returns {Promise<Object>} List of bulk sends
36
+ * @param {Object} [params]
37
+ * @param {number} [params.page]
38
+ * @param {number} [params.limit]
39
+ * @returns {Promise<Object>}
72
40
  */
73
41
  async function list(params = {}) {
74
- const queryParams = new URLSearchParams();
75
- if (params.page) queryParams.set("page", params.page);
76
- if (params.limit) queryParams.set("limit", params.limit);
77
-
78
- const queryString = queryParams.toString();
79
- const endpoint = `/bulk_sends${queryString ? `?${queryString}` : ""}`;
80
-
81
- return apiRequest(endpoint, {
82
- method: "GET",
83
- });
42
+ const qp = new URLSearchParams();
43
+ if (params.page) qp.set("page", params.page);
44
+ if (params.limit) qp.set("limit", params.limit);
45
+ const qs = qp.toString();
46
+ return req(`/bulk_sends${qs ? `?${qs}` : ""}`, { method: "GET" });
84
47
  }
85
48
 
86
49
  /**
87
50
  * Create a bulk send
88
- * @param {Object} params - Bulk send parameters
89
- * @param {string} params.templateId - Template ID to use
90
- * @param {Array<Object>} params.recipients - List of recipient data
51
+ * @param {Object} params
52
+ * @param {string} params.templateId - Template ID
53
+ * @param {Array<Object>} params.recipients - Recipient data rows
91
54
  * @param {string} [params.subject] - Email subject
92
55
  * @param {string} [params.message] - Email message
93
- * @returns {Promise<Object>} Created bulk send
56
+ * @returns {Promise<Object>}
94
57
  */
95
58
  async function create(params) {
96
- const {
97
- templateId,
98
- recipients,
99
- subject,
100
- message,
101
- ...rest
102
- } = params;
59
+ if (!params || typeof params !== "object") {
60
+ throw new Error("[xSignwell] bulkSend.create: params object is required");
61
+ }
62
+ if (!params.templateId || typeof params.templateId !== "string") {
63
+ throw new Error("[xSignwell] bulkSend.create: templateId (string) is required");
64
+ }
65
+ if (!Array.isArray(params.recipients) || params.recipients.length === 0) {
66
+ throw new Error("[xSignwell] bulkSend.create: recipients (non-empty array) is required");
67
+ }
68
+
69
+ const { templateId, recipients, subject, message, ...rest } = params;
103
70
 
104
71
  const body = {
105
72
  document_template_id: templateId,
@@ -111,31 +78,40 @@ export async function setupBulkSend(fastify, config) {
111
78
  if (subject) body.subject = subject;
112
79
  if (message) body.message = message;
113
80
 
114
- return apiRequest("/bulk_sends", {
81
+ return req("/bulk_sends", {
115
82
  method: "POST",
116
83
  body: JSON.stringify(body),
117
84
  });
118
85
  }
119
86
 
120
87
  /**
121
- * Get CSV template for bulk send
122
- * @param {string} templateId - Template ID
123
- * @returns {Promise<Object>} CSV template info
88
+ * Get CSV template for a bulk send
89
+ * @param {string} templateId
90
+ * @returns {Promise<Object>}
124
91
  */
125
92
  async function getCsvTemplate(templateId) {
126
- return apiRequest(`/bulk_sends/csv_template?document_template_id=${templateId}`, {
93
+ if (!templateId || typeof templateId !== "string") {
94
+ throw new Error("[xSignwell] bulkSend.getCsvTemplate: templateId (string) is required");
95
+ }
96
+ return req(`/bulk_sends/csv_template?document_template_id=${templateId}`, {
127
97
  method: "GET",
128
98
  });
129
99
  }
130
100
 
131
101
  /**
132
102
  * Validate CSV for bulk send
133
- * @param {string} templateId - Template ID
134
- * @param {string} csvContent - CSV content (base64 or raw)
135
- * @returns {Promise<Object>} Validation result
103
+ * @param {string} templateId
104
+ * @param {string} csvContent
105
+ * @returns {Promise<Object>}
136
106
  */
137
107
  async function validateCsv(templateId, csvContent) {
138
- return apiRequest("/bulk_sends/validate_csv", {
108
+ if (!templateId || typeof templateId !== "string") {
109
+ throw new Error("[xSignwell] bulkSend.validateCsv: templateId (string) is required");
110
+ }
111
+ if (!csvContent || typeof csvContent !== "string") {
112
+ throw new Error("[xSignwell] bulkSend.validateCsv: csvContent (string) is required");
113
+ }
114
+ return req("/bulk_sends/validate_csv", {
139
115
  method: "POST",
140
116
  body: JSON.stringify({
141
117
  document_template_id: templateId,
@@ -146,26 +122,23 @@ export async function setupBulkSend(fastify, config) {
146
122
 
147
123
  /**
148
124
  * Get documents from a bulk send
149
- * @param {string} bulkSendId - Bulk send ID
150
- * @param {Object} [params] - Query parameters
151
- * @param {number} [params.page] - Page number
152
- * @param {number} [params.limit] - Items per page
153
- * @returns {Promise<Object>} List of documents
125
+ * @param {string} bulkSendId
126
+ * @param {Object} [params]
127
+ * @returns {Promise<Object>}
154
128
  */
155
129
  async function getDocuments(bulkSendId, params = {}) {
156
- const queryParams = new URLSearchParams();
157
- if (params.page) queryParams.set("page", params.page);
158
- if (params.limit) queryParams.set("limit", params.limit);
159
-
160
- const queryString = queryParams.toString();
161
- const endpoint = `/bulk_sends/${bulkSendId}/documents${queryString ? `?${queryString}` : ""}`;
162
-
163
- return apiRequest(endpoint, {
130
+ if (!bulkSendId || typeof bulkSendId !== "string") {
131
+ throw new Error("[xSignwell] bulkSend.getDocuments: bulkSendId (string) is required");
132
+ }
133
+ const qp = new URLSearchParams();
134
+ if (params.page) qp.set("page", params.page);
135
+ if (params.limit) qp.set("limit", params.limit);
136
+ const qs = qp.toString();
137
+ return req(`/bulk_sends/${bulkSendId}/documents${qs ? `?${qs}` : ""}`, {
164
138
  method: "GET",
165
139
  });
166
140
  }
167
141
 
168
- // Add bulk send service to xsignwell namespace
169
142
  fastify.xsignwell.bulkSend = {
170
143
  get,
171
144
  list,
@@ -175,5 +148,5 @@ export async function setupBulkSend(fastify, config) {
175
148
  getDocuments,
176
149
  };
177
150
 
178
- console.info(" Bulk Send Service initialized");
151
+ fastify.log.info("[xSignwell] Bulk Send service initialized");
179
152
  }
@@ -6,76 +6,49 @@
6
6
  * @see https://developers.signwell.com/reference/post_api-v1-documents
7
7
  */
8
8
 
9
+ import { apiRequest } from "../apiRequest.js";
10
+
9
11
  /**
10
- * Setup documents service
11
- *
12
- * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
- * @param {Object} config - Plugin configuration
12
+ * @param {import('fastify').FastifyInstance} fastify
13
+ * @param {Object} config
14
14
  */
15
15
  export async function setupDocuments(fastify, config) {
16
- const { apiKey, baseUrl, testMode } = config;
16
+ const { testMode } = config;
17
17
 
18
- /**
19
- * Make an API request to SignWell
20
- * @param {string} endpoint - API endpoint
21
- * @param {Object} options - Fetch options
22
- * @returns {Promise<Object>} API response
23
- */
24
- async function apiRequest(endpoint, options = {}) {
25
- const url = `${baseUrl}${endpoint}`;
26
- const headers = {
27
- "X-Api-Key": apiKey,
28
- "Content-Type": "application/json",
29
- ...options.headers,
30
- };
31
-
32
- const response = await fetch(url, {
33
- ...options,
34
- headers,
35
- });
36
-
37
- if (!response.ok) {
38
- const errorText = await response.text();
39
- let errorData;
40
- try {
41
- errorData = JSON.parse(errorText);
42
- } catch {
43
- errorData = { message: errorText };
44
- }
45
- fastify.log.error({ endpoint, status: response.status, error: errorData }, "SignWell API error");
46
- throw new Error(errorData.message || `SignWell API error: ${response.status}`);
47
- }
48
-
49
- // Handle empty responses (like DELETE)
50
- const text = await response.text();
51
- if (!text) return { success: true };
52
-
53
- return JSON.parse(text);
18
+ function req(endpoint, options) {
19
+ return apiRequest(fastify, config, endpoint, options);
54
20
  }
55
21
 
56
22
  /**
57
23
  * Create a new document for signing
58
- * @param {Object} params - Document parameters
24
+ * @param {Object} params
59
25
  * @param {string} params.name - Document name
60
- * @param {Array<Object>} params.recipients - List of recipients
61
- * @param {Array<Object>} [params.files] - Files to include (base64 or URL)
62
- * @param {boolean} [params.draft] - Create as draft (default: false)
63
- * @param {boolean} [params.testMode] - Use test mode
26
+ * @param {Array<Object>} params.recipients - Recipients list
27
+ * @param {Array<Object>} [params.files] - Files (base64 or URL)
28
+ * @param {boolean} [params.draft] - Create as draft
64
29
  * @param {string} [params.subject] - Email subject
65
30
  * @param {string} [params.message] - Email message
66
31
  * @param {Object} [params.metadata] - Custom metadata
67
- * @returns {Promise<Object>} Created document
32
+ * @returns {Promise<Object>}
68
33
  */
69
34
  async function create(params) {
35
+ if (!params || typeof params !== "object") {
36
+ throw new Error("[xSignwell] documents.create: params object is required");
37
+ }
38
+ if (!params.name || typeof params.name !== "string") {
39
+ throw new Error("[xSignwell] documents.create: name (string) is required");
40
+ }
41
+ if (!Array.isArray(params.recipients) || params.recipients.length === 0) {
42
+ throw new Error("[xSignwell] documents.create: recipients (non-empty array) is required");
43
+ }
44
+ for (const r of params.recipients) {
45
+ if (!r.email) {
46
+ throw new Error("[xSignwell] documents.create: each recipient must have an email");
47
+ }
48
+ }
49
+
70
50
  const {
71
- name,
72
- recipients,
73
- files,
74
- draft = false,
75
- subject,
76
- message,
77
- metadata,
78
- ...rest
51
+ name, recipients, files, draft = false, subject, message, metadata, ...rest
79
52
  } = params;
80
53
 
81
54
  const body = {
@@ -104,7 +77,7 @@ export async function setupDocuments(fastify, config) {
104
77
  if (message) body.message = message;
105
78
  if (metadata) body.metadata = metadata;
106
79
 
107
- return apiRequest("/documents", {
80
+ return req("/documents", {
108
81
  method: "POST",
109
82
  body: JSON.stringify(body),
110
83
  });
@@ -112,25 +85,23 @@ export async function setupDocuments(fastify, config) {
112
85
 
113
86
  /**
114
87
  * Create a document from a template
115
- * @param {string} templateId - Template ID
116
- * @param {Object} params - Document parameters
117
- * @param {Array<Object>} params.recipients - List of recipients
118
- * @param {Object} [params.fields] - Field values to pre-fill
119
- * @param {boolean} [params.draft] - Create as draft
120
- * @param {string} [params.subject] - Email subject
121
- * @param {string} [params.message] - Email message
122
- * @param {Object} [params.metadata] - Custom metadata
123
- * @returns {Promise<Object>} Created document
88
+ * @param {string} templateId
89
+ * @param {Object} params
90
+ * @returns {Promise<Object>}
124
91
  */
125
92
  async function createFromTemplate(templateId, params) {
93
+ if (!templateId || typeof templateId !== "string") {
94
+ throw new Error("[xSignwell] documents.createFromTemplate: templateId (string) is required");
95
+ }
96
+ if (!params || typeof params !== "object") {
97
+ throw new Error("[xSignwell] documents.createFromTemplate: params object is required");
98
+ }
99
+ if (!Array.isArray(params.recipients) || params.recipients.length === 0) {
100
+ throw new Error("[xSignwell] documents.createFromTemplate: recipients (non-empty array) is required");
101
+ }
102
+
126
103
  const {
127
- recipients,
128
- fields,
129
- draft = false,
130
- subject,
131
- message,
132
- metadata,
133
- ...rest
104
+ recipients, fields, draft = false, subject, message, metadata, ...rest
134
105
  } = params;
135
106
 
136
107
  const body = {
@@ -151,7 +122,7 @@ export async function setupDocuments(fastify, config) {
151
122
  if (message) body.message = message;
152
123
  if (metadata) body.metadata = metadata;
153
124
 
154
- return apiRequest(`/document_templates/${templateId}/documents`, {
125
+ return req(`/document_templates/${templateId}/documents`, {
155
126
  method: "POST",
156
127
  body: JSON.stringify(body),
157
128
  });
@@ -159,23 +130,42 @@ export async function setupDocuments(fastify, config) {
159
130
 
160
131
  /**
161
132
  * Get a document by ID
162
- * @param {string} documentId - Document ID
163
- * @returns {Promise<Object>} Document details
133
+ * @param {string} documentId
134
+ * @returns {Promise<Object>}
164
135
  */
165
136
  async function get(documentId) {
166
- return apiRequest(`/documents/${documentId}`, {
167
- method: "GET",
168
- });
137
+ if (!documentId || typeof documentId !== "string") {
138
+ throw new Error("[xSignwell] documents.get: documentId (string) is required");
139
+ }
140
+ return req(`/documents/${documentId}`, { method: "GET" });
169
141
  }
170
142
 
171
143
  /**
172
- * Send a document (or update and send)
173
- * @param {string} documentId - Document ID
174
- * @param {Object} [params] - Optional updates before sending
175
- * @returns {Promise<Object>} Updated document
144
+ * List documents
145
+ * @param {Object} [params]
146
+ * @param {number} [params.page]
147
+ * @param {number} [params.limit]
148
+ * @returns {Promise<Object>}
149
+ */
150
+ async function list(params = {}) {
151
+ const qp = new URLSearchParams();
152
+ if (params.page) qp.set("page", params.page);
153
+ if (params.limit) qp.set("limit", params.limit);
154
+ const qs = qp.toString();
155
+ return req(`/documents${qs ? `?${qs}` : ""}`, { method: "GET" });
156
+ }
157
+
158
+ /**
159
+ * Send a document
160
+ * @param {string} documentId
161
+ * @param {Object} [params]
162
+ * @returns {Promise<Object>}
176
163
  */
177
164
  async function send(documentId, params = {}) {
178
- return apiRequest(`/documents/${documentId}/send`, {
165
+ if (!documentId || typeof documentId !== "string") {
166
+ throw new Error("[xSignwell] documents.send: documentId (string) is required");
167
+ }
168
+ return req(`/documents/${documentId}/send`, {
179
169
  method: "POST",
180
170
  body: JSON.stringify(params),
181
171
  });
@@ -183,49 +173,59 @@ export async function setupDocuments(fastify, config) {
183
173
 
184
174
  /**
185
175
  * Send a reminder to recipients who haven't signed
186
- * @param {string} documentId - Document ID
187
- * @returns {Promise<Object>} Result
176
+ * @param {string} documentId
177
+ * @returns {Promise<Object>}
188
178
  */
189
179
  async function remind(documentId) {
190
- return apiRequest(`/documents/${documentId}/remind`, {
191
- method: "POST",
192
- });
180
+ if (!documentId || typeof documentId !== "string") {
181
+ throw new Error("[xSignwell] documents.remind: documentId (string) is required");
182
+ }
183
+ return req(`/documents/${documentId}/remind`, { method: "POST" });
193
184
  }
194
185
 
195
186
  /**
196
187
  * Delete a document
197
- * @param {string} documentId - Document ID
198
- * @returns {Promise<Object>} Result
188
+ * @param {string} documentId
189
+ * @returns {Promise<Object>}
199
190
  */
200
191
  async function remove(documentId) {
201
- return apiRequest(`/documents/${documentId}`, {
202
- method: "DELETE",
203
- });
192
+ if (!documentId || typeof documentId !== "string") {
193
+ throw new Error("[xSignwell] documents.remove: documentId (string) is required");
194
+ }
195
+ return req(`/documents/${documentId}`, { method: "DELETE" });
204
196
  }
205
197
 
206
198
  /**
207
199
  * Download the completed PDF
208
- * @param {string} documentId - Document ID
209
- * @returns {Promise<Object>} PDF URL or base64 data
200
+ * @param {string} documentId
201
+ * @returns {Promise<Object>}
210
202
  */
211
203
  async function getCompletedPdf(documentId) {
212
- return apiRequest(`/documents/${documentId}/completed_pdf`, {
213
- method: "GET",
214
- });
204
+ if (!documentId || typeof documentId !== "string") {
205
+ throw new Error("[xSignwell] documents.getCompletedPdf: documentId (string) is required");
206
+ }
207
+ return req(`/documents/${documentId}/completed_pdf`, { method: "GET" });
215
208
  }
216
209
 
217
210
  /**
218
211
  * Get signing URL for embedded signing
219
- * @param {string} documentId - Document ID
220
- * @param {string} recipientId - Recipient ID
221
- * @returns {Promise<Object>} Embedded signing URL
212
+ * @param {string} documentId
213
+ * @param {string} recipientId
214
+ * @returns {Promise<Object>}
222
215
  */
223
216
  async function getEmbeddedSigningUrl(documentId, recipientId) {
217
+ if (!documentId || typeof documentId !== "string") {
218
+ throw new Error("[xSignwell] documents.getEmbeddedSigningUrl: documentId (string) is required");
219
+ }
220
+ if (!recipientId || typeof recipientId !== "string") {
221
+ throw new Error("[xSignwell] documents.getEmbeddedSigningUrl: recipientId (string) is required");
222
+ }
223
+
224
224
  const document = await get(documentId);
225
225
  const recipient = document.recipients?.find((r) => r.id === recipientId);
226
226
 
227
227
  if (!recipient) {
228
- throw new Error(`Recipient ${recipientId} not found in document`);
228
+ throw new Error(`[xSignwell] Recipient ${recipientId} not found in document ${documentId}`);
229
229
  }
230
230
 
231
231
  return {
@@ -234,18 +234,31 @@ export async function setupDocuments(fastify, config) {
234
234
  };
235
235
  }
236
236
 
237
- // Add documents service to xsignwell namespace
237
+ /**
238
+ * Get audit trail for a completed document
239
+ * @param {string} documentId
240
+ * @returns {Promise<Object>}
241
+ */
242
+ async function getAuditTrail(documentId) {
243
+ if (!documentId || typeof documentId !== "string") {
244
+ throw new Error("[xSignwell] documents.getAuditTrail: documentId (string) is required");
245
+ }
246
+ return req(`/documents/${documentId}/audit_trail`, { method: "GET" });
247
+ }
248
+
238
249
  fastify.xsignwell.documents = {
239
250
  create,
240
251
  createFromTemplate,
241
252
  get,
253
+ list,
242
254
  send,
243
255
  remind,
244
256
  remove,
245
- delete: remove, // Alias
257
+ delete: remove,
246
258
  getCompletedPdf,
247
259
  getEmbeddedSigningUrl,
260
+ getAuditTrail,
248
261
  };
249
262
 
250
- console.info(" Documents Service initialized");
263
+ fastify.log.info("[xSignwell] Documents service initialized");
251
264
  }