@xenterprises/fastify-xsignwell 1.1.0 → 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.
@@ -6,89 +6,51 @@
6
6
  * @see https://developers.signwell.com/reference/get_api-v1-document-templates-id
7
7
  */
8
8
 
9
+ import { apiRequest } from "../apiRequest.js";
10
+
9
11
  /**
10
- * Setup templates 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 setupTemplates(fastify, config) {
16
- const { apiKey, baseUrl } = 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 };
51
-
52
- return JSON.parse(text);
16
+ function req(endpoint, options) {
17
+ return apiRequest(fastify, config, endpoint, options);
53
18
  }
54
19
 
55
20
  /**
56
21
  * Get a template by ID
57
- * @param {string} templateId - Template ID
58
- * @returns {Promise<Object>} Template details
22
+ * @param {string} templateId
23
+ * @returns {Promise<Object>}
59
24
  */
60
25
  async function get(templateId) {
61
- return apiRequest(`/document_templates/${templateId}`, {
62
- method: "GET",
63
- });
26
+ if (!templateId || typeof templateId !== "string") {
27
+ throw new Error("[xSignwell] templates.get: templateId (string) is required");
28
+ }
29
+ return req(`/document_templates/${templateId}`, { method: "GET" });
64
30
  }
65
31
 
66
32
  /**
67
33
  * Create a new template
68
- * @param {Object} params - Template parameters
34
+ * @param {Object} params
69
35
  * @param {string} params.name - Template name
70
- * @param {Array<Object>} [params.files] - Files to include (base64 or URL)
36
+ * @param {Array<Object>} [params.files] - Files (base64 or URL)
71
37
  * @param {Array<Object>} [params.recipients] - Recipient placeholders
72
38
  * @param {Array<Object>} [params.fields] - Field definitions
73
39
  * @param {string} [params.subject] - Default email subject
74
40
  * @param {string} [params.message] - Default email message
75
- * @returns {Promise<Object>} Created template
41
+ * @returns {Promise<Object>}
76
42
  */
77
43
  async function create(params) {
78
- const {
79
- name,
80
- files,
81
- recipients,
82
- fields,
83
- subject,
84
- message,
85
- ...rest
86
- } = params;
87
-
88
- const body = {
89
- name,
90
- ...rest,
91
- };
44
+ if (!params || typeof params !== "object") {
45
+ throw new Error("[xSignwell] templates.create: params object is required");
46
+ }
47
+ if (!params.name || typeof params.name !== "string") {
48
+ throw new Error("[xSignwell] templates.create: name (string) is required");
49
+ }
50
+
51
+ const { name, files, recipients, fields, subject, message, ...rest } = params;
52
+
53
+ const body = { name, ...rest };
92
54
 
93
55
  if (files) {
94
56
  body.files = files.map((f) => ({
@@ -110,7 +72,7 @@ export async function setupTemplates(fastify, config) {
110
72
  if (subject) body.subject = subject;
111
73
  if (message) body.message = message;
112
74
 
113
- return apiRequest("/document_templates", {
75
+ return req("/document_templates", {
114
76
  method: "POST",
115
77
  body: JSON.stringify(body),
116
78
  });
@@ -118,12 +80,18 @@ export async function setupTemplates(fastify, config) {
118
80
 
119
81
  /**
120
82
  * Update an existing template
121
- * @param {string} templateId - Template ID
122
- * @param {Object} params - Update parameters
123
- * @returns {Promise<Object>} Updated template
83
+ * @param {string} templateId
84
+ * @param {Object} params
85
+ * @returns {Promise<Object>}
124
86
  */
125
87
  async function update(templateId, params) {
126
- return apiRequest(`/document_templates/${templateId}`, {
88
+ if (!templateId || typeof templateId !== "string") {
89
+ throw new Error("[xSignwell] templates.update: templateId (string) is required");
90
+ }
91
+ if (!params || typeof params !== "object") {
92
+ throw new Error("[xSignwell] templates.update: params object is required");
93
+ }
94
+ return req(`/document_templates/${templateId}`, {
127
95
  method: "PUT",
128
96
  body: JSON.stringify(params),
129
97
  });
@@ -131,66 +99,67 @@ export async function setupTemplates(fastify, config) {
131
99
 
132
100
  /**
133
101
  * Delete a template
134
- * @param {string} templateId - Template ID
135
- * @returns {Promise<Object>} Result
102
+ * @param {string} templateId
103
+ * @returns {Promise<Object>}
136
104
  */
137
105
  async function remove(templateId) {
138
- return apiRequest(`/document_templates/${templateId}`, {
139
- method: "DELETE",
140
- });
106
+ if (!templateId || typeof templateId !== "string") {
107
+ throw new Error("[xSignwell] templates.remove: templateId (string) is required");
108
+ }
109
+ return req(`/document_templates/${templateId}`, { method: "DELETE" });
141
110
  }
142
111
 
143
112
  /**
144
113
  * List all templates
145
- * @param {Object} [params] - Query parameters
146
- * @param {number} [params.page] - Page number
147
- * @param {number} [params.limit] - Items per page
148
- * @returns {Promise<Object>} List of templates
114
+ * @param {Object} [params]
115
+ * @param {number} [params.page]
116
+ * @param {number} [params.limit]
117
+ * @returns {Promise<Object>}
149
118
  */
150
119
  async function list(params = {}) {
151
- const queryParams = new URLSearchParams();
152
- if (params.page) queryParams.set("page", params.page);
153
- if (params.limit) queryParams.set("limit", params.limit);
154
-
155
- const queryString = queryParams.toString();
156
- const endpoint = `/document_templates${queryString ? `?${queryString}` : ""}`;
157
-
158
- return apiRequest(endpoint, {
159
- method: "GET",
160
- });
120
+ const qp = new URLSearchParams();
121
+ if (params.page) qp.set("page", params.page);
122
+ if (params.limit) qp.set("limit", params.limit);
123
+ const qs = qp.toString();
124
+ return req(`/document_templates${qs ? `?${qs}` : ""}`, { method: "GET" });
161
125
  }
162
126
 
163
127
  /**
164
- * Get template fields (for pre-filling)
165
- * @param {string} templateId - Template ID
166
- * @returns {Promise<Array>} List of fields
128
+ * Get template fields
129
+ * @param {string} templateId
130
+ * @returns {Promise<Array>}
167
131
  */
168
132
  async function getFields(templateId) {
133
+ if (!templateId || typeof templateId !== "string") {
134
+ throw new Error("[xSignwell] templates.getFields: templateId (string) is required");
135
+ }
169
136
  const template = await get(templateId);
170
137
  return template.fields || [];
171
138
  }
172
139
 
173
140
  /**
174
141
  * Get template recipients (placeholders)
175
- * @param {string} templateId - Template ID
176
- * @returns {Promise<Array>} List of recipient placeholders
142
+ * @param {string} templateId
143
+ * @returns {Promise<Array>}
177
144
  */
178
145
  async function getRecipients(templateId) {
146
+ if (!templateId || typeof templateId !== "string") {
147
+ throw new Error("[xSignwell] templates.getRecipients: templateId (string) is required");
148
+ }
179
149
  const template = await get(templateId);
180
150
  return template.recipients || [];
181
151
  }
182
152
 
183
- // Add templates service to xsignwell namespace
184
153
  fastify.xsignwell.templates = {
185
154
  get,
186
155
  create,
187
156
  update,
188
157
  remove,
189
- delete: remove, // Alias
158
+ delete: remove,
190
159
  list,
191
160
  getFields,
192
161
  getRecipients,
193
162
  };
194
163
 
195
- console.info(" Templates Service initialized");
164
+ fastify.log.info("[xSignwell] Templates service initialized");
196
165
  }
@@ -6,80 +6,46 @@
6
6
  * @see https://developers.signwell.com/reference/get_api-v1-hooks
7
7
  */
8
8
 
9
+ import { createHmac, timingSafeEqual } from "node:crypto";
10
+ import { apiRequest } from "../apiRequest.js";
11
+
9
12
  /**
10
- * Setup webhooks service
11
- *
12
- * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
- * @param {Object} config - Plugin configuration
13
+ * @param {import('fastify').FastifyInstance} fastify
14
+ * @param {Object} config
14
15
  */
15
16
  export async function setupWebhooks(fastify, config) {
16
- const { apiKey, baseUrl } = 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 };
51
-
52
- return JSON.parse(text);
17
+ function req(endpoint, options) {
18
+ return apiRequest(fastify, config, endpoint, options);
53
19
  }
54
20
 
55
21
  /**
56
22
  * List all webhooks
57
- * @returns {Promise<Array>} List of webhooks
23
+ * @returns {Promise<Array>}
58
24
  */
59
25
  async function list() {
60
- return apiRequest("/hooks", {
61
- method: "GET",
62
- });
26
+ return req("/hooks", { method: "GET" });
63
27
  }
64
28
 
65
29
  /**
66
30
  * Create a new webhook
67
- * @param {Object} params - Webhook parameters
68
- * @param {string} params.callbackUrl - URL to receive webhook events
31
+ * @param {Object} params
32
+ * @param {string} params.callbackUrl - URL to receive events
69
33
  * @param {string} [params.event] - Event type to subscribe to
70
- * @returns {Promise<Object>} Created webhook
34
+ * @returns {Promise<Object>}
71
35
  */
72
36
  async function create(params) {
73
- const { callbackUrl, event, ...rest } = params;
74
-
75
- const body = {
76
- callback_url: callbackUrl,
77
- ...rest,
78
- };
37
+ if (!params || typeof params !== "object") {
38
+ throw new Error("[xSignwell] webhooks.create: params object is required");
39
+ }
40
+ if (!params.callbackUrl || typeof params.callbackUrl !== "string") {
41
+ throw new Error("[xSignwell] webhooks.create: callbackUrl (string) is required");
42
+ }
79
43
 
44
+ const { callbackUrl, event, ...rest } = params;
45
+ const body = { callback_url: callbackUrl, ...rest };
80
46
  if (event) body.event = event;
81
47
 
82
- return apiRequest("/hooks", {
48
+ return req("/hooks", {
83
49
  method: "POST",
84
50
  body: JSON.stringify(body),
85
51
  });
@@ -87,18 +53,17 @@ export async function setupWebhooks(fastify, config) {
87
53
 
88
54
  /**
89
55
  * Delete a webhook
90
- * @param {string} webhookId - Webhook ID
91
- * @returns {Promise<Object>} Result
56
+ * @param {string} webhookId
57
+ * @returns {Promise<Object>}
92
58
  */
93
59
  async function remove(webhookId) {
94
- return apiRequest(`/hooks/${webhookId}`, {
95
- method: "DELETE",
96
- });
60
+ if (!webhookId || typeof webhookId !== "string") {
61
+ throw new Error("[xSignwell] webhooks.remove: webhookId (string) is required");
62
+ }
63
+ return req(`/hooks/${webhookId}`, { method: "DELETE" });
97
64
  }
98
65
 
99
- /**
100
- * Webhook event types
101
- */
66
+ /** Webhook event type constants */
102
67
  const events = {
103
68
  DOCUMENT_COMPLETED: "document.completed",
104
69
  DOCUMENT_SENT: "document.sent",
@@ -114,39 +79,41 @@ export async function setupWebhooks(fastify, config) {
114
79
  };
115
80
 
116
81
  /**
117
- * Verify webhook signature (if SignWell provides one)
82
+ * Verify webhook signature (HMAC-SHA256)
118
83
  * @param {string} payload - Raw request body
119
84
  * @param {string} signature - Signature header value
120
85
  * @param {string} secret - Webhook secret
121
- * @returns {boolean} True if valid
86
+ * @returns {boolean}
122
87
  */
123
88
  function verifySignature(payload, signature, secret) {
124
- // SignWell may use HMAC-SHA256 for webhook signatures
125
- // This is a placeholder - implement based on SignWell's actual signature method
126
- if (!signature || !secret) return true; // No signature verification if not configured
89
+ if (!payload || typeof payload !== "string") {
90
+ throw new Error("[xSignwell] webhooks.verifySignature: payload (string) is required");
91
+ }
92
+ if (!signature || typeof signature !== "string") {
93
+ throw new Error("[xSignwell] webhooks.verifySignature: signature (string) is required");
94
+ }
95
+ if (!secret || typeof secret !== "string") {
96
+ throw new Error("[xSignwell] webhooks.verifySignature: secret (string) is required");
97
+ }
127
98
 
128
99
  try {
129
- const crypto = require("crypto");
130
- const expectedSignature = crypto
131
- .createHmac("sha256", secret)
132
- .update(payload)
133
- .digest("hex");
134
- return crypto.timingSafeEqual(
135
- Buffer.from(signature),
136
- Buffer.from(expectedSignature)
137
- );
100
+ const expected = createHmac("sha256", secret).update(payload).digest("hex");
101
+ return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
138
102
  } catch (error) {
139
- fastify.log.error({ error }, "Failed to verify webhook signature");
103
+ fastify.log.error({ error: error.message }, "[xSignwell] Signature verification failed");
140
104
  return false;
141
105
  }
142
106
  }
143
107
 
144
108
  /**
145
109
  * Parse webhook event payload
146
- * @param {Object} payload - Webhook payload
147
- * @returns {Object} Parsed event data
110
+ * @param {Object} payload
111
+ * @returns {Object}
148
112
  */
149
113
  function parseEvent(payload) {
114
+ if (!payload || typeof payload !== "object") {
115
+ throw new Error("[xSignwell] webhooks.parseEvent: payload (object) is required");
116
+ }
150
117
  return {
151
118
  event: payload.event || payload.type,
152
119
  documentId: payload.document?.id || payload.document_id,
@@ -157,16 +124,15 @@ export async function setupWebhooks(fastify, config) {
157
124
  };
158
125
  }
159
126
 
160
- // Add webhooks service to xsignwell namespace
161
127
  fastify.xsignwell.webhooks = {
162
128
  list,
163
129
  create,
164
130
  remove,
165
- delete: remove, // Alias
131
+ delete: remove,
166
132
  events,
167
133
  verifySignature,
168
134
  parseEvent,
169
135
  };
170
136
 
171
- console.info(" Webhooks Service initialized");
137
+ fastify.log.info("[xSignwell] Webhooks service initialized");
172
138
  }
package/src/xSignwell.js CHANGED
@@ -2,12 +2,13 @@
2
2
  * xSignwell - SignWell Document Signing Integration
3
3
  *
4
4
  * A Fastify plugin for integrating with SignWell's e-signature API.
5
- * Provides document creation, template management, and webhook handling.
5
+ * Provides document creation, template management, bulk sending, and webhook handling.
6
6
  *
7
7
  * @see https://developers.signwell.com/reference/getting-started-with-your-api-1
8
8
  */
9
9
 
10
10
  import fp from "fastify-plugin";
11
+ import { apiRequest } from "./apiRequest.js";
11
12
  import { setupDocuments } from "./services/documents.js";
12
13
  import { setupTemplates } from "./services/templates.js";
13
14
  import { setupWebhooks } from "./services/webhooks.js";
@@ -24,15 +25,23 @@ import { setupBulkSend } from "./services/bulkSend.js";
24
25
  * @param {boolean} [options.active] - Enable/disable the plugin (default: true)
25
26
  */
26
27
  async function xSignwell(fastify, options) {
27
- // Check if plugin is disabled
28
28
  if (options.active === false) {
29
- console.info(" ⏸️ xSignwell Disabled");
29
+ fastify.log.info("[xSignwell] Plugin disabled");
30
30
  return;
31
31
  }
32
32
 
33
- // Validate required configuration
33
+ // Validate required options
34
34
  if (!options.apiKey) {
35
- throw new Error("xSignwell: apiKey is required");
35
+ throw new Error("[xSignwell] apiKey is required");
36
+ }
37
+ if (typeof options.apiKey !== "string") {
38
+ throw new Error("[xSignwell] apiKey must be a string");
39
+ }
40
+ if (options.baseUrl !== undefined && typeof options.baseUrl !== "string") {
41
+ throw new Error("[xSignwell] baseUrl must be a string");
42
+ }
43
+ if (options.testMode !== undefined && typeof options.testMode !== "boolean") {
44
+ throw new Error("[xSignwell] testMode must be a boolean");
36
45
  }
37
46
 
38
47
  const config = {
@@ -41,10 +50,7 @@ async function xSignwell(fastify, options) {
41
50
  testMode: options.testMode || false,
42
51
  };
43
52
 
44
- // Store configuration on fastify instance
45
- fastify.decorate("xsignwell", {
46
- config,
47
- });
53
+ fastify.decorate("xsignwell", { config });
48
54
 
49
55
  // Setup services
50
56
  await setupDocuments(fastify, config);
@@ -52,31 +58,18 @@ async function xSignwell(fastify, options) {
52
58
  await setupWebhooks(fastify, config);
53
59
  await setupBulkSend(fastify, config);
54
60
 
55
- // Add utility method to get current user/account info
61
+ // Account info helper
56
62
  fastify.xsignwell.me = async function () {
57
- const response = await fetch(`${config.baseUrl}/me`, {
58
- method: "GET",
59
- headers: {
60
- "X-Api-Key": config.apiKey,
61
- "Content-Type": "application/json",
62
- },
63
- });
64
-
65
- if (!response.ok) {
66
- const error = await response.text();
67
- fastify.log.error({ error }, "Failed to get SignWell account info");
68
- throw new Error(`SignWell API error: ${response.status}`);
69
- }
70
-
71
- return response.json();
63
+ return apiRequest(fastify, config, "/me", { method: "GET" });
72
64
  };
73
65
 
74
- console.info("xSignwell Document Signing Enabled");
66
+ fastify.log.info("[xSignwell] Document Signing Enabled");
75
67
  if (config.testMode) {
76
- console.info(" ⚠️ Running in TEST MODE");
68
+ fastify.log.info("[xSignwell] Running in TEST MODE");
77
69
  }
78
70
  }
79
71
 
80
72
  export default fp(xSignwell, {
81
73
  name: "xSignwell",
74
+ fastify: ">=5.0.0",
82
75
  });
package/.gitlab-ci.yml DELETED
@@ -1,45 +0,0 @@
1
- # ============================================================================
2
- # GitLab CI/CD Pipeline - xSignwell
3
- # ============================================================================
4
- # Runs tests on merge requests and commits to main/master
5
-
6
- stages:
7
- - test
8
-
9
- variables:
10
- NODE_ENV: test
11
-
12
- # ============================================================================
13
- # Shared Configuration
14
- # ============================================================================
15
- .shared_rules: &shared_rules
16
- rules:
17
- - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
18
- - if: '$CI_COMMIT_BRANCH == "main"'
19
- - if: '$CI_COMMIT_BRANCH == "master"'
20
- - if: '$CI_COMMIT_TAG'
21
-
22
- # ============================================================================
23
- # STAGE: TEST
24
- # ============================================================================
25
- test:
26
- stage: test
27
- image: node:20-alpine
28
- <<: *shared_rules
29
-
30
- cache:
31
- key: ${CI_COMMIT_REF_SLUG}
32
- paths:
33
- - node_modules/
34
-
35
- before_script:
36
- - npm ci
37
-
38
- script:
39
- - echo "Running xSignwell tests..."
40
- - npm test
41
- - npm audit --audit-level=high || true
42
-
43
- retry:
44
- max: 2
45
- when: runner_system_failure