@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.
@@ -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, '&amp;')
353
+ .replace(/</g, '&lt;')
354
+ .replace(/>/g, '&gt;')
355
+ .replace(/"/g, '&quot;')
356
+ .replace(/'/g, '&apos;');
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 };