@xenterprises/fastify-xsignwell 1.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.
@@ -0,0 +1,251 @@
1
+ /**
2
+ * SignWell Documents Service
3
+ *
4
+ * Handles document creation, retrieval, sending, and management.
5
+ *
6
+ * @see https://developers.signwell.com/reference/post_api-v1-documents
7
+ */
8
+
9
+ /**
10
+ * Setup documents service
11
+ *
12
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
+ * @param {Object} config - Plugin configuration
14
+ */
15
+ export async function setupDocuments(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
+ // Handle empty responses (like DELETE)
50
+ const text = await response.text();
51
+ if (!text) return { success: true };
52
+
53
+ return JSON.parse(text);
54
+ }
55
+
56
+ /**
57
+ * Create a new document for signing
58
+ * @param {Object} params - Document parameters
59
+ * @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
64
+ * @param {string} [params.subject] - Email subject
65
+ * @param {string} [params.message] - Email message
66
+ * @param {Object} [params.metadata] - Custom metadata
67
+ * @returns {Promise<Object>} Created document
68
+ */
69
+ async function create(params) {
70
+ const {
71
+ name,
72
+ recipients,
73
+ files,
74
+ draft = false,
75
+ subject,
76
+ message,
77
+ metadata,
78
+ ...rest
79
+ } = params;
80
+
81
+ const body = {
82
+ name,
83
+ recipients: recipients.map((r) => ({
84
+ id: r.id || `recipient_${Date.now()}_${Math.random().toString(36).slice(2)}`,
85
+ email: r.email,
86
+ name: r.name,
87
+ send_email: r.sendEmail !== false,
88
+ ...r,
89
+ })),
90
+ test_mode: testMode || params.testMode,
91
+ draft,
92
+ ...rest,
93
+ };
94
+
95
+ if (files) {
96
+ body.files = files.map((f) => ({
97
+ name: f.name,
98
+ file_base64: f.base64 || f.file_base64,
99
+ file_url: f.url || f.file_url,
100
+ }));
101
+ }
102
+
103
+ if (subject) body.subject = subject;
104
+ if (message) body.message = message;
105
+ if (metadata) body.metadata = metadata;
106
+
107
+ return apiRequest("/documents", {
108
+ method: "POST",
109
+ body: JSON.stringify(body),
110
+ });
111
+ }
112
+
113
+ /**
114
+ * 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
124
+ */
125
+ async function createFromTemplate(templateId, params) {
126
+ const {
127
+ recipients,
128
+ fields,
129
+ draft = false,
130
+ subject,
131
+ message,
132
+ metadata,
133
+ ...rest
134
+ } = params;
135
+
136
+ const body = {
137
+ recipients: recipients.map((r) => ({
138
+ id: r.id,
139
+ email: r.email,
140
+ name: r.name,
141
+ send_email: r.sendEmail !== false,
142
+ ...r,
143
+ })),
144
+ test_mode: testMode || params.testMode,
145
+ draft,
146
+ ...rest,
147
+ };
148
+
149
+ if (fields) body.fields = fields;
150
+ if (subject) body.subject = subject;
151
+ if (message) body.message = message;
152
+ if (metadata) body.metadata = metadata;
153
+
154
+ return apiRequest(`/document_templates/${templateId}/documents`, {
155
+ method: "POST",
156
+ body: JSON.stringify(body),
157
+ });
158
+ }
159
+
160
+ /**
161
+ * Get a document by ID
162
+ * @param {string} documentId - Document ID
163
+ * @returns {Promise<Object>} Document details
164
+ */
165
+ async function get(documentId) {
166
+ return apiRequest(`/documents/${documentId}`, {
167
+ method: "GET",
168
+ });
169
+ }
170
+
171
+ /**
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
176
+ */
177
+ async function send(documentId, params = {}) {
178
+ return apiRequest(`/documents/${documentId}/send`, {
179
+ method: "POST",
180
+ body: JSON.stringify(params),
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Send a reminder to recipients who haven't signed
186
+ * @param {string} documentId - Document ID
187
+ * @returns {Promise<Object>} Result
188
+ */
189
+ async function remind(documentId) {
190
+ return apiRequest(`/documents/${documentId}/remind`, {
191
+ method: "POST",
192
+ });
193
+ }
194
+
195
+ /**
196
+ * Delete a document
197
+ * @param {string} documentId - Document ID
198
+ * @returns {Promise<Object>} Result
199
+ */
200
+ async function remove(documentId) {
201
+ return apiRequest(`/documents/${documentId}`, {
202
+ method: "DELETE",
203
+ });
204
+ }
205
+
206
+ /**
207
+ * Download the completed PDF
208
+ * @param {string} documentId - Document ID
209
+ * @returns {Promise<Object>} PDF URL or base64 data
210
+ */
211
+ async function getCompletedPdf(documentId) {
212
+ return apiRequest(`/documents/${documentId}/completed_pdf`, {
213
+ method: "GET",
214
+ });
215
+ }
216
+
217
+ /**
218
+ * 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
222
+ */
223
+ async function getEmbeddedSigningUrl(documentId, recipientId) {
224
+ const document = await get(documentId);
225
+ const recipient = document.recipients?.find((r) => r.id === recipientId);
226
+
227
+ if (!recipient) {
228
+ throw new Error(`Recipient ${recipientId} not found in document`);
229
+ }
230
+
231
+ return {
232
+ url: recipient.embedded_signing_url,
233
+ recipient,
234
+ };
235
+ }
236
+
237
+ // Add documents service to xsignwell namespace
238
+ fastify.xsignwell.documents = {
239
+ create,
240
+ createFromTemplate,
241
+ get,
242
+ send,
243
+ remind,
244
+ remove,
245
+ delete: remove, // Alias
246
+ getCompletedPdf,
247
+ getEmbeddedSigningUrl,
248
+ };
249
+
250
+ console.info(" • Documents Service initialized");
251
+ }
@@ -0,0 +1,196 @@
1
+ /**
2
+ * SignWell Templates Service
3
+ *
4
+ * Handles template creation, retrieval, updating, and management.
5
+ *
6
+ * @see https://developers.signwell.com/reference/get_api-v1-document-templates-id
7
+ */
8
+
9
+ /**
10
+ * Setup templates service
11
+ *
12
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
+ * @param {Object} config - Plugin configuration
14
+ */
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);
53
+ }
54
+
55
+ /**
56
+ * Get a template by ID
57
+ * @param {string} templateId - Template ID
58
+ * @returns {Promise<Object>} Template details
59
+ */
60
+ async function get(templateId) {
61
+ return apiRequest(`/document_templates/${templateId}`, {
62
+ method: "GET",
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Create a new template
68
+ * @param {Object} params - Template parameters
69
+ * @param {string} params.name - Template name
70
+ * @param {Array<Object>} [params.files] - Files to include (base64 or URL)
71
+ * @param {Array<Object>} [params.recipients] - Recipient placeholders
72
+ * @param {Array<Object>} [params.fields] - Field definitions
73
+ * @param {string} [params.subject] - Default email subject
74
+ * @param {string} [params.message] - Default email message
75
+ * @returns {Promise<Object>} Created template
76
+ */
77
+ 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
+ };
92
+
93
+ if (files) {
94
+ body.files = files.map((f) => ({
95
+ name: f.name,
96
+ file_base64: f.base64 || f.file_base64,
97
+ file_url: f.url || f.file_url,
98
+ }));
99
+ }
100
+
101
+ if (recipients) {
102
+ body.recipients = recipients.map((r) => ({
103
+ id: r.id,
104
+ name: r.name || r.placeholder_name,
105
+ ...r,
106
+ }));
107
+ }
108
+
109
+ if (fields) body.fields = fields;
110
+ if (subject) body.subject = subject;
111
+ if (message) body.message = message;
112
+
113
+ return apiRequest("/document_templates", {
114
+ method: "POST",
115
+ body: JSON.stringify(body),
116
+ });
117
+ }
118
+
119
+ /**
120
+ * Update an existing template
121
+ * @param {string} templateId - Template ID
122
+ * @param {Object} params - Update parameters
123
+ * @returns {Promise<Object>} Updated template
124
+ */
125
+ async function update(templateId, params) {
126
+ return apiRequest(`/document_templates/${templateId}`, {
127
+ method: "PUT",
128
+ body: JSON.stringify(params),
129
+ });
130
+ }
131
+
132
+ /**
133
+ * Delete a template
134
+ * @param {string} templateId - Template ID
135
+ * @returns {Promise<Object>} Result
136
+ */
137
+ async function remove(templateId) {
138
+ return apiRequest(`/document_templates/${templateId}`, {
139
+ method: "DELETE",
140
+ });
141
+ }
142
+
143
+ /**
144
+ * 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
149
+ */
150
+ 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
+ });
161
+ }
162
+
163
+ /**
164
+ * Get template fields (for pre-filling)
165
+ * @param {string} templateId - Template ID
166
+ * @returns {Promise<Array>} List of fields
167
+ */
168
+ async function getFields(templateId) {
169
+ const template = await get(templateId);
170
+ return template.fields || [];
171
+ }
172
+
173
+ /**
174
+ * Get template recipients (placeholders)
175
+ * @param {string} templateId - Template ID
176
+ * @returns {Promise<Array>} List of recipient placeholders
177
+ */
178
+ async function getRecipients(templateId) {
179
+ const template = await get(templateId);
180
+ return template.recipients || [];
181
+ }
182
+
183
+ // Add templates service to xsignwell namespace
184
+ fastify.xsignwell.templates = {
185
+ get,
186
+ create,
187
+ update,
188
+ remove,
189
+ delete: remove, // Alias
190
+ list,
191
+ getFields,
192
+ getRecipients,
193
+ };
194
+
195
+ console.info(" • Templates Service initialized");
196
+ }
@@ -0,0 +1,172 @@
1
+ /**
2
+ * SignWell Webhooks Service
3
+ *
4
+ * Handles webhook registration, management, and event processing.
5
+ *
6
+ * @see https://developers.signwell.com/reference/get_api-v1-hooks
7
+ */
8
+
9
+ /**
10
+ * Setup webhooks service
11
+ *
12
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance
13
+ * @param {Object} config - Plugin configuration
14
+ */
15
+ 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);
53
+ }
54
+
55
+ /**
56
+ * List all webhooks
57
+ * @returns {Promise<Array>} List of webhooks
58
+ */
59
+ async function list() {
60
+ return apiRequest("/hooks", {
61
+ method: "GET",
62
+ });
63
+ }
64
+
65
+ /**
66
+ * Create a new webhook
67
+ * @param {Object} params - Webhook parameters
68
+ * @param {string} params.callbackUrl - URL to receive webhook events
69
+ * @param {string} [params.event] - Event type to subscribe to
70
+ * @returns {Promise<Object>} Created webhook
71
+ */
72
+ async function create(params) {
73
+ const { callbackUrl, event, ...rest } = params;
74
+
75
+ const body = {
76
+ callback_url: callbackUrl,
77
+ ...rest,
78
+ };
79
+
80
+ if (event) body.event = event;
81
+
82
+ return apiRequest("/hooks", {
83
+ method: "POST",
84
+ body: JSON.stringify(body),
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Delete a webhook
90
+ * @param {string} webhookId - Webhook ID
91
+ * @returns {Promise<Object>} Result
92
+ */
93
+ async function remove(webhookId) {
94
+ return apiRequest(`/hooks/${webhookId}`, {
95
+ method: "DELETE",
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Webhook event types
101
+ */
102
+ const events = {
103
+ DOCUMENT_COMPLETED: "document.completed",
104
+ DOCUMENT_SENT: "document.sent",
105
+ DOCUMENT_VIEWED: "document.viewed",
106
+ DOCUMENT_SIGNED: "document.signed",
107
+ DOCUMENT_DECLINED: "document.declined",
108
+ DOCUMENT_EXPIRED: "document.expired",
109
+ DOCUMENT_VOIDED: "document.voided",
110
+ RECIPIENT_COMPLETED: "recipient.completed",
111
+ RECIPIENT_VIEWED: "recipient.viewed",
112
+ RECIPIENT_SIGNED: "recipient.signed",
113
+ RECIPIENT_DECLINED: "recipient.declined",
114
+ };
115
+
116
+ /**
117
+ * Verify webhook signature (if SignWell provides one)
118
+ * @param {string} payload - Raw request body
119
+ * @param {string} signature - Signature header value
120
+ * @param {string} secret - Webhook secret
121
+ * @returns {boolean} True if valid
122
+ */
123
+ 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
127
+
128
+ 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
+ );
138
+ } catch (error) {
139
+ fastify.log.error({ error }, "Failed to verify webhook signature");
140
+ return false;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Parse webhook event payload
146
+ * @param {Object} payload - Webhook payload
147
+ * @returns {Object} Parsed event data
148
+ */
149
+ function parseEvent(payload) {
150
+ return {
151
+ event: payload.event || payload.type,
152
+ documentId: payload.document?.id || payload.document_id,
153
+ document: payload.document,
154
+ recipient: payload.recipient,
155
+ timestamp: payload.timestamp || new Date().toISOString(),
156
+ raw: payload,
157
+ };
158
+ }
159
+
160
+ // Add webhooks service to xsignwell namespace
161
+ fastify.xsignwell.webhooks = {
162
+ list,
163
+ create,
164
+ remove,
165
+ delete: remove, // Alias
166
+ events,
167
+ verifySignature,
168
+ parseEvent,
169
+ };
170
+
171
+ console.info(" • Webhooks Service initialized");
172
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * xSignwell - SignWell Document Signing Integration
3
+ *
4
+ * A Fastify plugin for integrating with SignWell's e-signature API.
5
+ * Provides document creation, template management, and webhook handling.
6
+ *
7
+ * @see https://developers.signwell.com/reference/getting-started-with-your-api-1
8
+ */
9
+
10
+ import fp from "fastify-plugin";
11
+ import { setupDocuments } from "./services/documents.js";
12
+ import { setupTemplates } from "./services/templates.js";
13
+ import { setupWebhooks } from "./services/webhooks.js";
14
+ import { setupBulkSend } from "./services/bulkSend.js";
15
+
16
+ /**
17
+ * xSignwell Plugin
18
+ *
19
+ * @param {import('fastify').FastifyInstance} fastify - Fastify instance
20
+ * @param {Object} options - Plugin options
21
+ * @param {string} options.apiKey - SignWell API key (required)
22
+ * @param {string} [options.baseUrl] - API base URL (default: https://www.signwell.com/api/v1)
23
+ * @param {boolean} [options.testMode] - Enable test mode (default: false)
24
+ * @param {boolean} [options.active] - Enable/disable the plugin (default: true)
25
+ */
26
+ async function xSignwell(fastify, options) {
27
+ // Check if plugin is disabled
28
+ if (options.active === false) {
29
+ console.info(" ⏸️ xSignwell Disabled");
30
+ return;
31
+ }
32
+
33
+ // Validate required configuration
34
+ if (!options.apiKey) {
35
+ throw new Error("xSignwell: apiKey is required");
36
+ }
37
+
38
+ const config = {
39
+ apiKey: options.apiKey,
40
+ baseUrl: options.baseUrl || "https://www.signwell.com/api/v1",
41
+ testMode: options.testMode || false,
42
+ };
43
+
44
+ // Store configuration on fastify instance
45
+ fastify.decorate("xsignwell", {
46
+ config,
47
+ });
48
+
49
+ // Setup services
50
+ await setupDocuments(fastify, config);
51
+ await setupTemplates(fastify, config);
52
+ await setupWebhooks(fastify, config);
53
+ await setupBulkSend(fastify, config);
54
+
55
+ // Add utility method to get current user/account info
56
+ 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();
72
+ };
73
+
74
+ console.info(" ✅ xSignwell Document Signing Enabled");
75
+ if (config.testMode) {
76
+ console.info(" ⚠️ Running in TEST MODE");
77
+ }
78
+ }
79
+
80
+ export default fp(xSignwell, {
81
+ name: "xSignwell",
82
+ });