@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.
- package/.gitlab-ci.yml +45 -0
- package/README.md +340 -0
- package/package.json +35 -0
- package/src/services/bulkSend.js +179 -0
- package/src/services/documents.js +251 -0
- package/src/services/templates.js +196 -0
- package/src/services/webhooks.js +172 -0
- package/src/xSignwell.js +82 -0
- package/test/xSignwell.test.js +539 -0
|
@@ -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
|
+
}
|
package/src/xSignwell.js
ADDED
|
@@ -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
|
+
});
|