@certysign/sdk 2.3.0 → 2.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/README.md +76 -0
- package/package.json +54 -54
- package/src/index.js +228 -214
- package/src/lib/BillingResource.js +90 -0
- package/src/lib/EnvelopeResource.js +306 -249
- package/src/lib/HashSigningResource.js +20 -5
- package/src/lib/HttpClient.js +211 -198
- package/src/lib/IdentityResource.js +143 -0
- package/src/lib/SignerResource.js +120 -0
- package/src/lib/SigningResource.js +11 -7
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* client.billing — what the organisation has, and what things cost.
|
|
3
|
+
*
|
|
4
|
+
* Read-only by design. Topping up, changing plan and editing auto-top-up are done
|
|
5
|
+
* from the CertySign dashboard by a person, not by an API key: a key is a bearer
|
|
6
|
+
* token that can leak, and a leaked key must not be able to spend or to change what
|
|
7
|
+
* the organisation is billed.
|
|
8
|
+
*
|
|
9
|
+
* Requires the `billing:read` scope.
|
|
10
|
+
*/
|
|
11
|
+
class BillingResource {
|
|
12
|
+
constructor(http) {
|
|
13
|
+
this._http = http;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Token balance and wallet standing.
|
|
18
|
+
*
|
|
19
|
+
* `availableBalance` is the number to spend against — `balance` includes tokens
|
|
20
|
+
* already reserved by operations in flight. A `status` of 'suspended' means
|
|
21
|
+
* chargeable calls are refused even when the balance looks healthy.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const { data } = await client.billing.wallet();
|
|
25
|
+
* console.log(`${data.availableBalance} ${data.currency} available`);
|
|
26
|
+
*/
|
|
27
|
+
wallet() {
|
|
28
|
+
return this._http.get('/sdk/v1/billing/wallet');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* What an operation will cost, and whether you can currently cover it.
|
|
33
|
+
*
|
|
34
|
+
* Call this BEFORE a large batch. One answer up front beats discovering
|
|
35
|
+
* INSUFFICIENT_BALANCE halfway through, with some documents signed and charged
|
|
36
|
+
* and others not.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @param {string} opts.operationType e.g. 'document_signing', 'certificate_issuance'
|
|
40
|
+
* @param {string} [opts.operationSubType] e.g. 'ADVANCED', 'ADES'
|
|
41
|
+
* @param {number} [opts.quantity=1] how many — the total is priced for all of them
|
|
42
|
+
* @param {number} [opts.additionalSigners=0]
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const { data } = await client.billing.quote({
|
|
46
|
+
* operationType: 'document_signing',
|
|
47
|
+
* operationSubType: 'ADVANCED',
|
|
48
|
+
* quantity: documents.length,
|
|
49
|
+
* });
|
|
50
|
+
* if (!data.canAfford) throw new Error(`Need ${data.tokensTotal}, have ${data.availableBalance}`);
|
|
51
|
+
*/
|
|
52
|
+
quote({ operationType, operationSubType, quantity = 1, additionalSigners = 0 } = {}) {
|
|
53
|
+
if (!operationType) throw new Error('billing.quote: operationType is required');
|
|
54
|
+
const qs = new URLSearchParams({
|
|
55
|
+
operationType,
|
|
56
|
+
...(operationSubType && { operationSubType }),
|
|
57
|
+
quantity: String(quantity),
|
|
58
|
+
additionalSigners: String(additionalSigners),
|
|
59
|
+
});
|
|
60
|
+
return this._http.get(`/sdk/v1/billing/quote?${qs.toString()}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Wallet history — what was charged, for what, and when.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} [opts]
|
|
67
|
+
* @param {number} [opts.limit=50] capped at 200
|
|
68
|
+
* @param {number} [opts.skip=0]
|
|
69
|
+
* @param {string} [opts.type] filter by transaction type
|
|
70
|
+
* @param {string} [opts.status] filter by status
|
|
71
|
+
*/
|
|
72
|
+
transactions({ limit = 50, skip = 0, type, status } = {}) {
|
|
73
|
+
const qs = new URLSearchParams({ limit: String(limit), skip: String(skip) });
|
|
74
|
+
if (type) qs.set('type', type);
|
|
75
|
+
if (status) qs.set('status', status);
|
|
76
|
+
return this._http.get(`/sdk/v1/billing/transactions?${qs.toString()}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The subscription plan and the feature flags that come with it. */
|
|
80
|
+
plan() {
|
|
81
|
+
return this._http.get('/sdk/v1/billing/plan');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The full price list, for showing costs without a call per item. */
|
|
85
|
+
pricing() {
|
|
86
|
+
return this._http.get('/sdk/v1/billing/pricing');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { BillingResource };
|
|
@@ -1,249 +1,306 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview EnvelopeResource — full envelope lifecycle management
|
|
3
|
-
*
|
|
4
|
-
* Covers the complete envelope CRUD plus document management:
|
|
5
|
-
* - create() POST /sdk/v1/envelopes
|
|
6
|
-
* - get() GET /sdk/v1/envelopes/:id
|
|
7
|
-
* - list() GET /sdk/v1/envelopes
|
|
8
|
-
* - uploadDocuments() POST /sdk/v1/envelopes/:id/documents
|
|
9
|
-
* - send() POST /sdk/v1/envelopes/:id/send
|
|
10
|
-
* - sign() POST /sdk/v1/envelopes/:id/sign
|
|
11
|
-
* - getDocument() GET /sdk/v1/envelopes/:id/documents/:docId
|
|
12
|
-
* - getAuditTrail() GET /sdk/v1/envelopes/:id/audit
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
'use strict';
|
|
16
|
-
|
|
17
|
-
const FormData = require('form-data');
|
|
18
|
-
|
|
19
|
-
class EnvelopeResource {
|
|
20
|
-
/** @param {import('../lib/HttpClient').HttpClient} http */
|
|
21
|
-
constructor(http) {
|
|
22
|
-
this._http = http;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ── Create ─────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Create a new signing envelope.
|
|
29
|
-
*
|
|
30
|
-
* Envelopes are containers for documents and signature requests.
|
|
31
|
-
* After creation, upload documents (uploadDocuments), then send
|
|
32
|
-
* the envelope (send) to make it ready for signing.
|
|
33
|
-
*
|
|
34
|
-
* @param {CreateEnvelopeOptions} options
|
|
35
|
-
* @returns {Promise<EnvelopeResult>}
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* const { data } = await client.envelopes.create({
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* name: 'Dr. Grace Mwangi',
|
|
42
|
-
* email: 'g.mwangi@nhif.or.ke',
|
|
43
|
-
* role: 'primary'
|
|
44
|
-
* }],
|
|
45
|
-
* metadata: { claimPeriod: 'Q1-2026', department: 'Reimbursements' }
|
|
46
|
-
* });
|
|
47
|
-
* const envelopeId = data.envelope._id;
|
|
48
|
-
*/
|
|
49
|
-
create(options = {}) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// ──
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
* @param {
|
|
133
|
-
* @returns {Promise<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* @
|
|
154
|
-
*
|
|
155
|
-
* @
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
* }
|
|
160
|
-
*
|
|
161
|
-
*/
|
|
162
|
-
|
|
163
|
-
if (!envelopeId)
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* the
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
* @
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview EnvelopeResource — full envelope lifecycle management
|
|
3
|
+
*
|
|
4
|
+
* Covers the complete envelope CRUD plus document management:
|
|
5
|
+
* - create() POST /sdk/v1/envelopes
|
|
6
|
+
* - get() GET /sdk/v1/envelopes/:id
|
|
7
|
+
* - list() GET /sdk/v1/envelopes
|
|
8
|
+
* - uploadDocuments() POST /sdk/v1/envelopes/:id/documents
|
|
9
|
+
* - send() POST /sdk/v1/envelopes/:id/send
|
|
10
|
+
* - sign() POST /sdk/v1/envelopes/:id/sign
|
|
11
|
+
* - getDocument() GET /sdk/v1/envelopes/:id/documents/:docId
|
|
12
|
+
* - getAuditTrail() GET /sdk/v1/envelopes/:id/audit
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const FormData = require('form-data');
|
|
18
|
+
|
|
19
|
+
class EnvelopeResource {
|
|
20
|
+
/** @param {import('../lib/HttpClient').HttpClient} http */
|
|
21
|
+
constructor(http) {
|
|
22
|
+
this._http = http;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ── Create ─────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a new signing envelope.
|
|
29
|
+
*
|
|
30
|
+
* Envelopes are containers for documents and signature requests.
|
|
31
|
+
* After creation, upload documents (uploadDocuments), then send
|
|
32
|
+
* the envelope (send) to make it ready for signing.
|
|
33
|
+
*
|
|
34
|
+
* @param {CreateEnvelopeOptions} options
|
|
35
|
+
* @returns {Promise<EnvelopeResult>}
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const { data } = await client.envelopes.create({
|
|
39
|
+
* name: 'NHIF Reimbursement Claim Q1-2026',
|
|
40
|
+
* recipients: [{
|
|
41
|
+
* name: 'Dr. Grace Mwangi',
|
|
42
|
+
* email: 'g.mwangi@nhif.or.ke',
|
|
43
|
+
* role: 'primary'
|
|
44
|
+
* }],
|
|
45
|
+
* metadata: { claimPeriod: 'Q1-2026', department: 'Reimbursements' }
|
|
46
|
+
* });
|
|
47
|
+
* const envelopeId = data.envelope._id;
|
|
48
|
+
*/
|
|
49
|
+
create(options = {}) {
|
|
50
|
+
// The API reads `name` and `recipients`. This used to post `title` and `signers`,
|
|
51
|
+
// which the server simply ignored — producing an unnamed envelope with nobody on
|
|
52
|
+
// it, which then failed at send time with NO_SIGNERS and no hint as to why. Both
|
|
53
|
+
// spellings are accepted so existing callers keep working, and both are mapped to
|
|
54
|
+
// what the server actually reads.
|
|
55
|
+
const {
|
|
56
|
+
title, name = title,
|
|
57
|
+
signers, recipients = signers || [],
|
|
58
|
+
message, description = message,
|
|
59
|
+
metadata = {},
|
|
60
|
+
signingOrder, signingSequence, requires2FA, requiresBiometric,
|
|
61
|
+
biometricFallback, expiresAt,
|
|
62
|
+
} = options;
|
|
63
|
+
|
|
64
|
+
if (!name) throw new Error('envelopes.create: name (or title) is required');
|
|
65
|
+
|
|
66
|
+
return this._http.post('/sdk/v1/envelopes', {
|
|
67
|
+
data: {
|
|
68
|
+
name,
|
|
69
|
+
...(description !== undefined && { description }),
|
|
70
|
+
recipients,
|
|
71
|
+
metadata,
|
|
72
|
+
...(signingOrder !== undefined && { signingOrder }),
|
|
73
|
+
...(signingSequence !== undefined && { signingSequence }),
|
|
74
|
+
...(requires2FA !== undefined && { requires2FA }),
|
|
75
|
+
...(requiresBiometric !== undefined && { requiresBiometric }),
|
|
76
|
+
...(biometricFallback !== undefined && { biometricFallback }),
|
|
77
|
+
...(expiresAt !== undefined && { expiresAt }),
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ── Send ───────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Send a draft envelope to its recipients for signature.
|
|
86
|
+
*
|
|
87
|
+
* Requires the `envelope:send` scope. The envelope must be a draft with at least
|
|
88
|
+
* one document and one recipient — envelopes produced by the signing endpoints are
|
|
89
|
+
* already complete and cannot be sent.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* await client.envelopes.send(envelopeId);
|
|
93
|
+
*/
|
|
94
|
+
send(envelopeId) {
|
|
95
|
+
if (!envelopeId) throw new Error('envelopes.send: envelopeId is required');
|
|
96
|
+
return this._http.post(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/send`, { data: {} });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Verify an envelope's audit hash chain.
|
|
101
|
+
*
|
|
102
|
+
* Requires the `audit:verify` scope. Works on any envelope, however it was
|
|
103
|
+
* produced — an immediate signing call or a collected signing workflow.
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const { data } = await client.envelopes.verifyAuditChain(envelopeId);
|
|
107
|
+
* if (!data.valid) throw new Error('Audit trail has been altered');
|
|
108
|
+
*/
|
|
109
|
+
verifyAuditChain(envelopeId) {
|
|
110
|
+
if (!envelopeId) throw new Error('envelopes.verifyAuditChain: envelopeId is required');
|
|
111
|
+
return this._http.post(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/audit/verify`, { data: {} });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Get ────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get a single envelope by ID with full status, signers, and document list.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} envelopeId
|
|
120
|
+
* @returns {Promise<EnvelopeResult>}
|
|
121
|
+
*/
|
|
122
|
+
get(envelopeId) {
|
|
123
|
+
if (!envelopeId) throw new Error('envelopes.get: envelopeId is required');
|
|
124
|
+
return this._http.get(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ── List ───────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* List envelopes for the authenticated tenant.
|
|
131
|
+
*
|
|
132
|
+
* @param {ListEnvelopesOptions} [options]
|
|
133
|
+
* @returns {Promise<ListEnvelopesResult>}
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const { data } = await client.envelopes.list({ status: 'completed', limit: 20 });
|
|
137
|
+
* data.envelopes.forEach(env => console.log(env._id, env.status));
|
|
138
|
+
*/
|
|
139
|
+
list(options = {}) {
|
|
140
|
+
const { status, page = 1, limit = 20, search } = options;
|
|
141
|
+
return this._http.get('/sdk/v1/envelopes', {
|
|
142
|
+
params: { status, page, limit, search }
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Upload Documents ───────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Upload one or more PDF documents to an existing envelope.
|
|
150
|
+
*
|
|
151
|
+
* Must be called after create() and before send().
|
|
152
|
+
*
|
|
153
|
+
* @param {string} envelopeId
|
|
154
|
+
* @param {DocumentUpload[]} documents
|
|
155
|
+
* @returns {Promise<UploadResult>}
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* await client.envelopes.uploadDocuments('env_abc123', [
|
|
159
|
+
* { data: fs.readFileSync('./report.pdf'), filename: 'Q1-report.pdf' }
|
|
160
|
+
* ]);
|
|
161
|
+
*/
|
|
162
|
+
uploadDocuments(envelopeId, documents) {
|
|
163
|
+
if (!envelopeId) throw new Error('envelopes.uploadDocuments: envelopeId is required');
|
|
164
|
+
if (!documents?.length) throw new Error('envelopes.uploadDocuments: documents array is required');
|
|
165
|
+
|
|
166
|
+
const form = new FormData();
|
|
167
|
+
for (const doc of documents) {
|
|
168
|
+
const buf = doc.data;
|
|
169
|
+
const name = doc.filename || 'document.pdf';
|
|
170
|
+
if (Buffer.isBuffer(buf)) {
|
|
171
|
+
form.append('documents', buf, { filename: name, contentType: 'application/pdf' });
|
|
172
|
+
} else if (buf && typeof buf.pipe === 'function') {
|
|
173
|
+
form.append('documents', buf, { filename: name, contentType: 'application/pdf' });
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error(`Document "${name}" must be a Buffer or ReadStream`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return this._http.post(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/documents`, {
|
|
180
|
+
formData: form
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ── Send ───────────────────────────────────────────────────────────────────
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Transition an envelope to 'sent' status, making it ready for signing.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} envelopeId
|
|
190
|
+
* @returns {Promise<EnvelopeResult>}
|
|
191
|
+
*/
|
|
192
|
+
send(envelopeId) {
|
|
193
|
+
if (!envelopeId) throw new Error('envelopes.send: envelopeId is required');
|
|
194
|
+
return this._http.post(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/send`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ── Sign ───────────────────────────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Apply a cryptographic signature to all documents in the envelope.
|
|
201
|
+
*
|
|
202
|
+
* This is the core signing step. For SDK requests, interactive 2FA is
|
|
203
|
+
* bypassed — the API key alone authorises the signature.
|
|
204
|
+
*
|
|
205
|
+
* The resulting signatures are PAdES-LTV compliant (PDF Advanced Electronic
|
|
206
|
+
* Signatures with Long-Term Validation).
|
|
207
|
+
*
|
|
208
|
+
* @param {string} envelopeId
|
|
209
|
+
* @param {SignEnvelopeOptions} [options]
|
|
210
|
+
* @returns {Promise<SignResult>}
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* const result = await client.envelopes.sign('env_abc123', {
|
|
214
|
+
* reason: 'Claims approval — NHIF Kenya',
|
|
215
|
+
* location: 'Nairobi, Kenya'
|
|
216
|
+
* });
|
|
217
|
+
* console.log(result.data.signatures[0].certificate.serialNumber);
|
|
218
|
+
*/
|
|
219
|
+
sign(envelopeId, options = {}) {
|
|
220
|
+
if (!envelopeId) throw new Error('envelopes.sign: envelopeId is required');
|
|
221
|
+
const { reason = 'Digital signature', location = 'Nairobi, Kenya' } = options;
|
|
222
|
+
return this._http.post(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/sign`, {
|
|
223
|
+
data: { reason, location }
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Download Document ──────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Download a signed document as a Buffer.
|
|
231
|
+
*
|
|
232
|
+
* @param {string} envelopeId
|
|
233
|
+
* @param {string} documentId
|
|
234
|
+
* @returns {Promise<Buffer>} Raw PDF bytes
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* const pdfBuffer = await client.envelopes.getDocument('env_abc123', 'doc_xyz789');
|
|
238
|
+
* fs.writeFileSync('./signed-claim.pdf', pdfBuffer);
|
|
239
|
+
*/
|
|
240
|
+
async getDocument(envelopeId, documentId) {
|
|
241
|
+
if (!envelopeId) throw new Error('envelopes.getDocument: envelopeId is required');
|
|
242
|
+
if (!documentId) throw new Error('envelopes.getDocument: documentId is required');
|
|
243
|
+
|
|
244
|
+
const response = await this._http.get(
|
|
245
|
+
`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/documents/${encodeURIComponent(documentId)}`,
|
|
246
|
+
{ responseType: 'arraybuffer' }
|
|
247
|
+
);
|
|
248
|
+
return Buffer.isBuffer(response) ? response : Buffer.from(response);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Audit Trail ────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Retrieve the cryptographically verifiable audit trail for an envelope.
|
|
255
|
+
*
|
|
256
|
+
* The audit chain is a Merkle-linked log of all envelope events:
|
|
257
|
+
* - created, document_uploaded, sent, signed, certificate_issued, completed
|
|
258
|
+
*
|
|
259
|
+
* Each entry includes a SHA-256 event hash and chain hash linking it to
|
|
260
|
+
* the previous entry, making the log tamper-evident.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} envelopeId
|
|
263
|
+
* @returns {Promise<AuditTrailResult>}
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* const { data } = await client.envelopes.getAuditTrail('env_abc123');
|
|
267
|
+
* data.auditTrail.forEach(event => {
|
|
268
|
+
* console.log(event.timestamp, event.action, event.eventHash);
|
|
269
|
+
* });
|
|
270
|
+
* if (data.chainIntegrity.valid) console.log('Audit chain intact');
|
|
271
|
+
*/
|
|
272
|
+
getAuditTrail(envelopeId) {
|
|
273
|
+
if (!envelopeId) throw new Error('envelopes.getAuditTrail: envelopeId is required');
|
|
274
|
+
return this._http.get(`/sdk/v1/envelopes/${encodeURIComponent(envelopeId)}/audit`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* @typedef {Object} CreateEnvelopeOptions
|
|
280
|
+
* @property {string} title - Envelope title
|
|
281
|
+
* @property {{ name: string, email?: string, role?: string }[]} [signers]
|
|
282
|
+
* @property {string} [message] - Message to signers
|
|
283
|
+
* @property {Object} [metadata]
|
|
284
|
+
*/
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @typedef {Object} DocumentUpload
|
|
288
|
+
* @property {Buffer|import('fs').ReadStream} data
|
|
289
|
+
* @property {string} [filename]
|
|
290
|
+
*/
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* @typedef {Object} SignEnvelopeOptions
|
|
294
|
+
* @property {string} [reason]
|
|
295
|
+
* @property {string} [location]
|
|
296
|
+
*/
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @typedef {Object} ListEnvelopesOptions
|
|
300
|
+
* @property {'draft'|'sent'|'in_progress'|'completed'|'cancelled'} [status]
|
|
301
|
+
* @property {number} [page]
|
|
302
|
+
* @property {number} [limit]
|
|
303
|
+
* @property {string} [search]
|
|
304
|
+
*/
|
|
305
|
+
|
|
306
|
+
module.exports = { EnvelopeResource };
|