@certysign/sdk 1.0.0 → 2.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/README.md +994 -55
- package/package.json +14 -6
- package/src/index.js +55 -20
- package/src/lib/CertificateResource.js +24 -3
- package/src/lib/DashboardResource.js +91 -0
- package/src/lib/DocumentHasher.js +123 -0
- package/src/lib/HashSigningResource.js +220 -0
- package/src/lib/HttpClient.js +1 -1
- package/src/lib/SignatureEmbedder.js +426 -0
- package/src/lib/SigningSessionResource.js +183 -0
- package/examples/certificate-flow.js +0 -198
- package/examples/dha-integration.js +0 -208
- package/examples/nhif-batch-sign.js +0 -216
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SignatureEmbedder — local signature embedding
|
|
3
|
+
*
|
|
4
|
+
* Embeds CMS/PKCS#7 signatures into documents on the subscriber's system.
|
|
5
|
+
* Documents NEVER leave the subscriber's infrastructure.
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - PDF (PAdES-style with pdf-lib) — single or multi-recipient visual stamps
|
|
9
|
+
* - XML (XMLDSig enveloped signature)
|
|
10
|
+
* - JSON (JWS-like detached signature)
|
|
11
|
+
*
|
|
12
|
+
* Visual stamp format (matches the CertySign platform):
|
|
13
|
+
* ┌──────────────────────────────────────────┐
|
|
14
|
+
* │ Digitally signed by: user@example.com │
|
|
15
|
+
* │ Date: 2026-03-13T11:07:28.927Z │
|
|
16
|
+
* │ Certificate: 9EF9C8E88478FC08C6759942 │
|
|
17
|
+
* │ Standard: PAdES Baseline B-B │
|
|
18
|
+
* └──────────────────────────────────────────┘
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
|
|
25
|
+
// ── Constants ──
|
|
26
|
+
const STAMP_FONT_SIZE = 7;
|
|
27
|
+
const STAMP_LINE_HEIGHT = 10;
|
|
28
|
+
const STAMP_PADDING_X = 8;
|
|
29
|
+
const STAMP_PADDING_Y = 8;
|
|
30
|
+
const STAMP_WIDTH = 260;
|
|
31
|
+
const STAMP_MARGIN = 6; // gap between multiple stamps
|
|
32
|
+
|
|
33
|
+
class SignatureEmbedder {
|
|
34
|
+
/**
|
|
35
|
+
* Embed one or more digital signatures into a PDF document.
|
|
36
|
+
*
|
|
37
|
+
* Supports two call patterns:
|
|
38
|
+
* 1. Single signature — pass options with a single `signature` field
|
|
39
|
+
* 2. Multiple signer — pass options with a `signatures[]` array
|
|
40
|
+
*
|
|
41
|
+
* Each signature gets its own visual stamp matching the CertySign platform:
|
|
42
|
+
* Digitally signed by: <email>
|
|
43
|
+
* Date: <ISO 8601>
|
|
44
|
+
* Certificate: <serial>
|
|
45
|
+
* Standard: PAdES Baseline B-B
|
|
46
|
+
*
|
|
47
|
+
* Multiple stamps are stacked from the bottom of the page upward,
|
|
48
|
+
* or positioned explicitly via `signaturePosition`.
|
|
49
|
+
*
|
|
50
|
+
* @param {Buffer} pdfBuffer - Original unsigned PDF
|
|
51
|
+
* @param {EmbedPdfOptions} options
|
|
52
|
+
* @returns {Promise<Buffer>} - Signed PDF buffer
|
|
53
|
+
*/
|
|
54
|
+
async embedInPdf(pdfBuffer, options) {
|
|
55
|
+
if (!pdfBuffer) throw new Error('embedInPdf: pdfBuffer is required');
|
|
56
|
+
|
|
57
|
+
let PDFDocument, PDFName, PDFHexString, PDFDict, rgb, StandardFonts;
|
|
58
|
+
try {
|
|
59
|
+
const pdfLib = require('pdf-lib');
|
|
60
|
+
PDFDocument = pdfLib.PDFDocument;
|
|
61
|
+
PDFName = pdfLib.PDFName;
|
|
62
|
+
PDFHexString = pdfLib.PDFHexString;
|
|
63
|
+
PDFDict = pdfLib.PDFDict;
|
|
64
|
+
rgb = pdfLib.rgb;
|
|
65
|
+
StandardFonts = pdfLib.StandardFonts;
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(
|
|
68
|
+
'pdf-lib is required for PDF signature embedding. Install it: npm install pdf-lib'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pdfDoc = await PDFDocument.load(pdfBuffer, { ignoreEncryption: true });
|
|
73
|
+
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
74
|
+
const pages = pdfDoc.getPages();
|
|
75
|
+
|
|
76
|
+
// ── Normalise to an array of signature entries ──
|
|
77
|
+
const sigEntries = options.signatures
|
|
78
|
+
? options.signatures
|
|
79
|
+
: [options];
|
|
80
|
+
|
|
81
|
+
if (sigEntries.length === 0) {
|
|
82
|
+
throw new Error('embedInPdf: at least one signature is required');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Track how many stamps we've drawn per page for auto-stacking
|
|
86
|
+
const pageStampCounts = {};
|
|
87
|
+
|
|
88
|
+
for (let idx = 0; idx < sigEntries.length; idx++) {
|
|
89
|
+
const entry = sigEntries[idx];
|
|
90
|
+
const sig = entry.signature;
|
|
91
|
+
if (!sig) throw new Error(`embedInPdf: signature is required (entry ${idx})`);
|
|
92
|
+
|
|
93
|
+
const signerEmail = entry.signerEmail || entry.recipientEmail || entry.signerName || 'CertySign';
|
|
94
|
+
const certSerial = entry.certSerialNumber || '';
|
|
95
|
+
const signDate = entry.timestamp ? new Date(entry.timestamp) :
|
|
96
|
+
entry.signedAt ? new Date(entry.signedAt) : new Date();
|
|
97
|
+
const reason = entry.reason || 'Digital signature';
|
|
98
|
+
const location = entry.location || '';
|
|
99
|
+
const standard = entry.standard || 'PAdES Baseline B-B';
|
|
100
|
+
|
|
101
|
+
// ── Determine target page ──
|
|
102
|
+
const pos = entry.signaturePosition || {};
|
|
103
|
+
const pageIdx = pos.page
|
|
104
|
+
? Math.min(pos.page - 1, pages.length - 1)
|
|
105
|
+
: pages.length - 1;
|
|
106
|
+
const sigPage = pages[pageIdx];
|
|
107
|
+
|
|
108
|
+
// ── Auto-stacking: each stamp stacks above the previous one ──
|
|
109
|
+
const stampKey = pageIdx;
|
|
110
|
+
if (!(stampKey in pageStampCounts)) pageStampCounts[stampKey] = 0;
|
|
111
|
+
const stampIndex = pageStampCounts[stampKey];
|
|
112
|
+
pageStampCounts[stampKey]++;
|
|
113
|
+
|
|
114
|
+
// 4 text lines = height
|
|
115
|
+
const stampHeight = (4 * STAMP_LINE_HEIGHT) + (2 * STAMP_PADDING_Y);
|
|
116
|
+
const stampWidth = pos.width || STAMP_WIDTH;
|
|
117
|
+
|
|
118
|
+
// Position: explicit position or auto-stack from bottom-left
|
|
119
|
+
const x = pos.x != null ? pos.x : 20;
|
|
120
|
+
const baseY = pos.y != null ? pos.y : 20;
|
|
121
|
+
const y = baseY + stampIndex * (stampHeight + STAMP_MARGIN);
|
|
122
|
+
|
|
123
|
+
// ── Draw stamp box ──
|
|
124
|
+
sigPage.drawRectangle({
|
|
125
|
+
x, y,
|
|
126
|
+
width: stampWidth,
|
|
127
|
+
height: stampHeight,
|
|
128
|
+
borderColor: rgb(0.4, 0.4, 0.4),
|
|
129
|
+
borderWidth: 0.75,
|
|
130
|
+
color: rgb(0.97, 0.97, 0.97),
|
|
131
|
+
opacity: 0.95
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ── Draw text lines matching CertySign platform format ──
|
|
135
|
+
const textLines = [
|
|
136
|
+
`Digitally signed by: ${signerEmail}`,
|
|
137
|
+
`Date: ${signDate.toISOString()}`,
|
|
138
|
+
`Certificate: ${certSerial}`,
|
|
139
|
+
`Standard: ${standard}`
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
let textY = y + stampHeight - STAMP_PADDING_Y - STAMP_FONT_SIZE;
|
|
143
|
+
for (const line of textLines) {
|
|
144
|
+
sigPage.drawText(line, {
|
|
145
|
+
x: x + STAMP_PADDING_X,
|
|
146
|
+
y: textY,
|
|
147
|
+
size: STAMP_FONT_SIZE,
|
|
148
|
+
font,
|
|
149
|
+
color: rgb(0.4, 0.4, 0.4),
|
|
150
|
+
maxWidth: stampWidth - (2 * STAMP_PADDING_X)
|
|
151
|
+
});
|
|
152
|
+
textY -= STAMP_LINE_HEIGHT;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ── Embed signature metadata as document properties ──
|
|
157
|
+
// Store the last (or primary) signature's CMS data for programmatic verification
|
|
158
|
+
const primary = sigEntries[0];
|
|
159
|
+
const sigMetadata = {
|
|
160
|
+
'CertySign-SignatureCount': String(sigEntries.length),
|
|
161
|
+
'CertySign-Algorithm': primary.algorithm || 'SHA256withRSA',
|
|
162
|
+
'CertySign-CertSerial': primary.certSerialNumber || '',
|
|
163
|
+
'CertySign-Timestamp': (primary.timestamp ? new Date(primary.timestamp) : new Date()).toISOString(),
|
|
164
|
+
'CertySign-Signer': primary.signerEmail || primary.recipientEmail || primary.signerName || 'CertySign',
|
|
165
|
+
'CertySign-DocumentHash': primary.documentHash || '',
|
|
166
|
+
'CertySign-HashAlgorithm': primary.hashAlgorithm || 'sha256',
|
|
167
|
+
'CertySign-Standard': primary.standard || 'PAdES Baseline B-B'
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Store each signature in metadata
|
|
171
|
+
for (let i = 0; i < sigEntries.length; i++) {
|
|
172
|
+
const entry = sigEntries[i];
|
|
173
|
+
sigMetadata[`CertySign-Signature-${i}`] = entry.signature;
|
|
174
|
+
if (entry.certSerialNumber) {
|
|
175
|
+
sigMetadata[`CertySign-CertSerial-${i}`] = entry.certSerialNumber;
|
|
176
|
+
}
|
|
177
|
+
const signer = entry.signerEmail || entry.recipientEmail || entry.signerName || '';
|
|
178
|
+
if (signer) sigMetadata[`CertySign-Signer-${i}`] = signer;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (primary.certificate) {
|
|
182
|
+
sigMetadata['CertySign-Certificate'] = primary.certificate.replace(/\n/g, '\\n');
|
|
183
|
+
}
|
|
184
|
+
if (primary.chain) {
|
|
185
|
+
sigMetadata['CertySign-Chain'] = primary.chain.replace(/\n/g, '\\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const infoDict = pdfDoc.context.lookup(pdfDoc.context.trailerInfo.Info);
|
|
189
|
+
if (infoDict instanceof PDFDict) {
|
|
190
|
+
for (const [key, value] of Object.entries(sigMetadata)) {
|
|
191
|
+
infoDict.set(PDFName.of(key), PDFHexString.fromText(value));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pdfDoc.setProducer('CertySign Trust Services');
|
|
196
|
+
pdfDoc.setModificationDate(new Date());
|
|
197
|
+
|
|
198
|
+
const signedBytes = await pdfDoc.save();
|
|
199
|
+
return Buffer.from(signedBytes);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Embed digital signatures into an XML document.
|
|
204
|
+
*
|
|
205
|
+
* Uses XMLDSig enveloped signature format.
|
|
206
|
+
* Supports multiple signatures — each signer gets their own <ds:Signature> element.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} xmlString - Original XML content
|
|
209
|
+
* @param {EmbedXmlOptions} options
|
|
210
|
+
* @returns {string} - Signed XML string
|
|
211
|
+
*/
|
|
212
|
+
embedInXml(xmlString, options) {
|
|
213
|
+
if (!xmlString) throw new Error('embedInXml: xmlString is required');
|
|
214
|
+
|
|
215
|
+
const sigEntries = options.signatures ? options.signatures : [options];
|
|
216
|
+
|
|
217
|
+
let result = xmlString;
|
|
218
|
+
|
|
219
|
+
for (let idx = 0; idx < sigEntries.length; idx++) {
|
|
220
|
+
const entry = sigEntries[idx];
|
|
221
|
+
const {
|
|
222
|
+
signature,
|
|
223
|
+
certificate,
|
|
224
|
+
documentHash,
|
|
225
|
+
hashAlgorithm = 'sha256',
|
|
226
|
+
certSerialNumber = '',
|
|
227
|
+
timestamp
|
|
228
|
+
} = entry;
|
|
229
|
+
|
|
230
|
+
if (!signature) throw new Error(`embedInXml: signature is required (entry ${idx})`);
|
|
231
|
+
|
|
232
|
+
const signerEmail = entry.signerEmail || entry.recipientEmail || entry.signerName || 'CertySign';
|
|
233
|
+
const standard = entry.standard || 'PAdES Baseline B-B';
|
|
234
|
+
const signDate = timestamp ? new Date(timestamp) : entry.signedAt ? new Date(entry.signedAt) : new Date();
|
|
235
|
+
|
|
236
|
+
const algorithmUri = {
|
|
237
|
+
sha256: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256',
|
|
238
|
+
sha384: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384',
|
|
239
|
+
sha512: 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'
|
|
240
|
+
}[hashAlgorithm] || 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256';
|
|
241
|
+
|
|
242
|
+
const digestUri = {
|
|
243
|
+
sha256: 'http://www.w3.org/2001/04/xmlenc#sha256',
|
|
244
|
+
sha384: 'http://www.w3.org/2001/04/xmldsig-more#sha384',
|
|
245
|
+
sha512: 'http://www.w3.org/2001/04/xmlenc#sha512'
|
|
246
|
+
}[hashAlgorithm] || 'http://www.w3.org/2001/04/xmlenc#sha256';
|
|
247
|
+
|
|
248
|
+
const certValue = certificate
|
|
249
|
+
? certificate.replace(/-----BEGIN CERTIFICATE-----/g, '')
|
|
250
|
+
.replace(/-----END CERTIFICATE-----/g, '')
|
|
251
|
+
.replace(/\s/g, '')
|
|
252
|
+
: '';
|
|
253
|
+
|
|
254
|
+
const signatureXml = `
|
|
255
|
+
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="CertySign-Signature-${idx}">
|
|
256
|
+
<ds:SignedInfo>
|
|
257
|
+
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
|
|
258
|
+
<ds:SignatureMethod Algorithm="${algorithmUri}"/>
|
|
259
|
+
<ds:Reference URI="">
|
|
260
|
+
<ds:Transforms>
|
|
261
|
+
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
|
|
262
|
+
</ds:Transforms>
|
|
263
|
+
<ds:DigestMethod Algorithm="${digestUri}"/>
|
|
264
|
+
<ds:DigestValue>${documentHash ? Buffer.from(documentHash, 'hex').toString('base64') : ''}</ds:DigestValue>
|
|
265
|
+
</ds:Reference>
|
|
266
|
+
</ds:SignedInfo>
|
|
267
|
+
<ds:SignatureValue>${signature}</ds:SignatureValue>
|
|
268
|
+
<ds:KeyInfo>
|
|
269
|
+
<ds:X509Data>
|
|
270
|
+
<ds:X509Certificate>${certValue}</ds:X509Certificate>
|
|
271
|
+
<ds:X509SerialNumber>${certSerialNumber}</ds:X509SerialNumber>
|
|
272
|
+
</ds:X509Data>
|
|
273
|
+
</ds:KeyInfo>
|
|
274
|
+
<ds:Object>
|
|
275
|
+
<SignatureProperties xmlns="urn:certysign:signature:1.0">
|
|
276
|
+
<SignerEmail>${_xmlEscape(signerEmail)}</SignerEmail>
|
|
277
|
+
<Timestamp>${signDate.toISOString()}</Timestamp>
|
|
278
|
+
<CertSerial>${_xmlEscape(certSerialNumber)}</CertSerial>
|
|
279
|
+
<Standard>${_xmlEscape(standard)}</Standard>
|
|
280
|
+
<Provider>CertySign</Provider>
|
|
281
|
+
</SignatureProperties>
|
|
282
|
+
</ds:Object>
|
|
283
|
+
</ds:Signature>`;
|
|
284
|
+
|
|
285
|
+
// Insert before closing root tag
|
|
286
|
+
const closingTagMatch = result.match(/<\/([^\s>]+)\s*>\s*$/);
|
|
287
|
+
if (closingTagMatch) {
|
|
288
|
+
const insertPos = result.lastIndexOf(closingTagMatch[0]);
|
|
289
|
+
result = result.substring(0, insertPos) + signatureXml + '\n' + closingTagMatch[0];
|
|
290
|
+
} else {
|
|
291
|
+
result += signatureXml;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Create a signed JSON envelope with one or more signatures.
|
|
300
|
+
*
|
|
301
|
+
* Supports multiple signers — each signer's signature is stored
|
|
302
|
+
* in the `signatures[]` array alongside the original data.
|
|
303
|
+
*
|
|
304
|
+
* @param {Object|string} jsonData - Original JSON content
|
|
305
|
+
* @param {EmbedJsonOptions} options
|
|
306
|
+
* @returns {Object} - Signed JSON envelope
|
|
307
|
+
*/
|
|
308
|
+
embedInJson(jsonData, options) {
|
|
309
|
+
const data = typeof jsonData === 'string' ? JSON.parse(jsonData) : jsonData;
|
|
310
|
+
const sigEntries = options.signatures ? options.signatures : [options];
|
|
311
|
+
|
|
312
|
+
const signaturesArray = sigEntries.map((entry, idx) => {
|
|
313
|
+
if (!entry.signature) throw new Error(`embedInJson: signature is required (entry ${idx})`);
|
|
314
|
+
|
|
315
|
+
const signerEmail = entry.signerEmail || entry.recipientEmail || entry.signerName || 'CertySign';
|
|
316
|
+
const signDate = entry.timestamp ? new Date(entry.timestamp) : entry.signedAt ? new Date(entry.signedAt) : new Date();
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
value: entry.signature,
|
|
320
|
+
algorithm: entry.algorithm || 'SHA256withRSA',
|
|
321
|
+
hashAlgorithm: entry.hashAlgorithm || 'sha256',
|
|
322
|
+
documentHash: entry.documentHash || '',
|
|
323
|
+
timestamp: signDate.toISOString(),
|
|
324
|
+
signer: {
|
|
325
|
+
email: signerEmail,
|
|
326
|
+
name: entry.recipientName || entry.signerName || signerEmail,
|
|
327
|
+
certSerialNumber: entry.certSerialNumber || ''
|
|
328
|
+
},
|
|
329
|
+
certificate: entry.certificate || null,
|
|
330
|
+
chain: entry.chain || null,
|
|
331
|
+
standard: entry.standard || 'PAdES Baseline B-B'
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
data,
|
|
337
|
+
signatures: signaturesArray,
|
|
338
|
+
metadata: {
|
|
339
|
+
provider: 'CertySign Trust Services',
|
|
340
|
+
version: '2.0.0',
|
|
341
|
+
signatureCount: signaturesArray.length,
|
|
342
|
+
signedAt: new Date().toISOString()
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
function _xmlEscape(str) {
|
|
351
|
+
return String(str)
|
|
352
|
+
.replace(/&/g, '&')
|
|
353
|
+
.replace(/</g, '<')
|
|
354
|
+
.replace(/>/g, '>')
|
|
355
|
+
.replace(/"/g, '"')
|
|
356
|
+
.replace(/'/g, ''');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* @typedef {Object} SignatureEntry
|
|
361
|
+
* @property {string} signature - Base64 CMS/PKCS#7 from CertySign
|
|
362
|
+
* @property {string} [signerEmail] - Signer email (used for visual stamp)
|
|
363
|
+
* @property {string} [recipientEmail] - Alias for signerEmail
|
|
364
|
+
* @property {string} [signerName] - Fallback signer name
|
|
365
|
+
* @property {string} [recipientName] - Signer display name
|
|
366
|
+
* @property {string} [certSerialNumber] - Certificate serial number
|
|
367
|
+
* @property {string} [certificate] - PEM certificate
|
|
368
|
+
* @property {string} [chain] - PEM chain
|
|
369
|
+
* @property {string} [documentHash] - Hex document hash
|
|
370
|
+
* @property {string} [hashAlgorithm]
|
|
371
|
+
* @property {string} [algorithm]
|
|
372
|
+
* @property {string} [reason]
|
|
373
|
+
* @property {string} [location]
|
|
374
|
+
* @property {string} [standard] - Default: 'PAdES Baseline B-B'
|
|
375
|
+
* @property {string} [timestamp]
|
|
376
|
+
* @property {string} [signedAt]
|
|
377
|
+
* @property {{ page?: number, x?: number, y?: number, width?: number }} [signaturePosition]
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* @typedef {Object} EmbedPdfOptions
|
|
382
|
+
* @property {string} [signature] - Single signature (use this OR signatures[])
|
|
383
|
+
* @property {SignatureEntry[]} [signatures] - Multiple signatures for multi-recipient
|
|
384
|
+
* @property {string} [signerEmail]
|
|
385
|
+
* @property {string} [certificate]
|
|
386
|
+
* @property {string} [chain]
|
|
387
|
+
* @property {string} [reason]
|
|
388
|
+
* @property {string} [signerName]
|
|
389
|
+
* @property {string} [location]
|
|
390
|
+
* @property {string} [certSerialNumber]
|
|
391
|
+
* @property {string} [documentHash]
|
|
392
|
+
* @property {string} [hashAlgorithm]
|
|
393
|
+
* @property {string} [algorithm]
|
|
394
|
+
* @property {string} [standard]
|
|
395
|
+
* @property {string} [timestamp]
|
|
396
|
+
* @property {{ page?: number, x?: number, y?: number, width?: number }} [signaturePosition]
|
|
397
|
+
*/
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* @typedef {Object} EmbedXmlOptions
|
|
401
|
+
* @property {string} [signature]
|
|
402
|
+
* @property {SignatureEntry[]} [signatures]
|
|
403
|
+
* @property {string} [certificate]
|
|
404
|
+
* @property {string} [chain]
|
|
405
|
+
* @property {string} [documentHash]
|
|
406
|
+
* @property {string} [hashAlgorithm]
|
|
407
|
+
* @property {string} [signerEmail]
|
|
408
|
+
* @property {string} [certSerialNumber]
|
|
409
|
+
* @property {string} [timestamp]
|
|
410
|
+
*/
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* @typedef {Object} EmbedJsonOptions
|
|
414
|
+
* @property {string} [signature]
|
|
415
|
+
* @property {SignatureEntry[]} [signatures]
|
|
416
|
+
* @property {string} [certificate]
|
|
417
|
+
* @property {string} [chain]
|
|
418
|
+
* @property {string} [documentHash]
|
|
419
|
+
* @property {string} [hashAlgorithm]
|
|
420
|
+
* @property {string} [algorithm]
|
|
421
|
+
* @property {string} [signerEmail]
|
|
422
|
+
* @property {string} [certSerialNumber]
|
|
423
|
+
* @property {string} [timestamp]
|
|
424
|
+
*/
|
|
425
|
+
|
|
426
|
+
module.exports = { SignatureEmbedder };
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SigningSessionResource — multi-recipient hash-based signing
|
|
3
|
+
*
|
|
4
|
+
* Manages signing sessions where:
|
|
5
|
+
* - Document hashes are registered (documents stay local)
|
|
6
|
+
* - Recipients are added with sequential or parallel signing order
|
|
7
|
+
* - OTP verification is required before each recipient can sign
|
|
8
|
+
* - HSM signs the hash for each verified recipient
|
|
9
|
+
*
|
|
10
|
+
* Covers:
|
|
11
|
+
* - create() Create a signing session
|
|
12
|
+
* - get() Get session status
|
|
13
|
+
* - list() List sessions
|
|
14
|
+
* - sendOtp() Send OTP to a recipient
|
|
15
|
+
* - verifyOtp() Verify recipient OTP
|
|
16
|
+
* - recipientSign() Recipient signs after OTP verification
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
class SigningSessionResource {
|
|
22
|
+
/** @param {import('./HttpClient').HttpClient} http */
|
|
23
|
+
constructor(http) {
|
|
24
|
+
this._http = http;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a signing session with document hashes and recipients.
|
|
29
|
+
*
|
|
30
|
+
* @param {CreateSessionOptions} options
|
|
31
|
+
* @returns {Promise<Object>}
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const session = await client.sessions.create({
|
|
35
|
+
* name: 'Q1 Contract',
|
|
36
|
+
* documents: [
|
|
37
|
+
* { hash: 'abc123...', fileName: 'contract.pdf', hashAlgorithm: 'sha256' }
|
|
38
|
+
* ],
|
|
39
|
+
* recipients: [
|
|
40
|
+
* { email: 'ceo@example.com', name: 'Jane CEO', role: 'signer', order: 1 },
|
|
41
|
+
* { email: 'cfo@example.com', name: 'John CFO', role: 'signer', order: 2 }
|
|
42
|
+
* ],
|
|
43
|
+
* signingOrder: 'sequential'
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* console.log(session.data._id); // Session ID
|
|
47
|
+
* console.log(session.data.recipients[0].recipientId); // Use for OTP/sign
|
|
48
|
+
*/
|
|
49
|
+
create(options) {
|
|
50
|
+
const {
|
|
51
|
+
name,
|
|
52
|
+
description,
|
|
53
|
+
documents,
|
|
54
|
+
recipients,
|
|
55
|
+
signingOrder = 'parallel',
|
|
56
|
+
expiresAt
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!name) throw new Error('create: name is required');
|
|
60
|
+
if (!documents?.length) throw new Error('create: documents array is required');
|
|
61
|
+
if (!recipients?.length) throw new Error('create: recipients array is required');
|
|
62
|
+
|
|
63
|
+
return this._http.post('/sdk/v1/signing-sessions', {
|
|
64
|
+
data: { name, description, documents, recipients, signingOrder, expiresAt }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a signing session by ID.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} sessionId
|
|
72
|
+
* @returns {Promise<Object>}
|
|
73
|
+
*/
|
|
74
|
+
get(sessionId) {
|
|
75
|
+
if (!sessionId) throw new Error('get: sessionId is required');
|
|
76
|
+
return this._http.get(`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List signing sessions with pagination.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {number} [options.page=1]
|
|
84
|
+
* @param {number} [options.limit=20]
|
|
85
|
+
* @param {string} [options.status]
|
|
86
|
+
* @returns {Promise<Object>}
|
|
87
|
+
*/
|
|
88
|
+
list(options = {}) {
|
|
89
|
+
return this._http.get('/sdk/v1/signing-sessions', { params: options });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Send an OTP verification code to a recipient's email.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} sessionId
|
|
96
|
+
* @param {string} recipientId
|
|
97
|
+
* @returns {Promise<Object>}
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* await client.sessions.sendOtp(sessionId, recipientId);
|
|
101
|
+
* // Recipient receives a 6-digit code via email
|
|
102
|
+
*/
|
|
103
|
+
sendOtp(sessionId, recipientId) {
|
|
104
|
+
if (!sessionId) throw new Error('sendOtp: sessionId is required');
|
|
105
|
+
if (!recipientId) throw new Error('sendOtp: recipientId is required');
|
|
106
|
+
return this._http.post(
|
|
107
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/send-otp`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Verify a recipient's OTP code.
|
|
113
|
+
* On success, returns a short-lived signing token.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} sessionId
|
|
116
|
+
* @param {string} recipientId
|
|
117
|
+
* @param {string} code - 6-digit OTP from email
|
|
118
|
+
* @returns {Promise<Object>} - { signingToken, expiresAt }
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const result = await client.sessions.verifyOtp(sessionId, recipientId, '123456');
|
|
122
|
+
* const signingToken = result.data.signingToken;
|
|
123
|
+
*/
|
|
124
|
+
verifyOtp(sessionId, recipientId, code) {
|
|
125
|
+
if (!sessionId) throw new Error('verifyOtp: sessionId is required');
|
|
126
|
+
if (!recipientId) throw new Error('verifyOtp: recipientId is required');
|
|
127
|
+
if (!code) throw new Error('verifyOtp: code is required');
|
|
128
|
+
|
|
129
|
+
return this._http.post(
|
|
130
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/verify-otp`,
|
|
131
|
+
{ data: { code: String(code) } }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recipient signs all documents in the session.
|
|
137
|
+
* Requires the signing token obtained from verifyOtp().
|
|
138
|
+
*
|
|
139
|
+
* @param {string} sessionId
|
|
140
|
+
* @param {string} recipientId
|
|
141
|
+
* @param {string} signingToken - Token from verifyOtp()
|
|
142
|
+
* @returns {Promise<Object>} - { signedDocuments, certificate, chain }
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const result = await client.sessions.recipientSign(
|
|
146
|
+
* sessionId, recipientId, signingToken
|
|
147
|
+
* );
|
|
148
|
+
*
|
|
149
|
+
* // Use SignatureEmbedder to embed signatures into local documents
|
|
150
|
+
* for (const doc of result.data.signedDocuments) {
|
|
151
|
+
* const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
152
|
+
* signature: doc.signature,
|
|
153
|
+
* certificate: result.data.certificate,
|
|
154
|
+
* chain: result.data.chain,
|
|
155
|
+
* reason: 'Contract signing',
|
|
156
|
+
* signerName: 'Jane CEO'
|
|
157
|
+
* });
|
|
158
|
+
* fs.writeFileSync(`./signed-${doc.fileName}`, signedPdf);
|
|
159
|
+
* }
|
|
160
|
+
*/
|
|
161
|
+
recipientSign(sessionId, recipientId, signingToken) {
|
|
162
|
+
if (!sessionId) throw new Error('recipientSign: sessionId is required');
|
|
163
|
+
if (!recipientId) throw new Error('recipientSign: recipientId is required');
|
|
164
|
+
if (!signingToken) throw new Error('recipientSign: signingToken is required');
|
|
165
|
+
|
|
166
|
+
return this._http.post(
|
|
167
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/sign`,
|
|
168
|
+
{ data: { signingToken } }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {Object} CreateSessionOptions
|
|
175
|
+
* @property {string} name - Session name
|
|
176
|
+
* @property {string} [description]
|
|
177
|
+
* @property {{ hash: string, fileName: string, hashAlgorithm?: string, mimeType?: string }[]} documents
|
|
178
|
+
* @property {{ email: string, name: string, role?: string, order?: number }[]} recipients
|
|
179
|
+
* @property {'sequential'|'parallel'} [signingOrder='parallel']
|
|
180
|
+
* @property {string} [expiresAt] - ISO date
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
module.exports = { SigningSessionResource };
|