@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 CHANGED
@@ -1,6 +1,37 @@
1
1
  # @certysign/sdk
2
2
 
3
- Official Node.js SDK for **CertySign Trust Services** — digital document signing, X.509 certificate issuance, and PKI operations built for East Africa.
3
+ Official Node.js SDK for **CertySign Trust Services** — hash-based digital signing, multi-recipient signing sessions, X.509 certificate management, and PKI operations built for East Africa.
4
+
5
+ **Documents never leave your infrastructure.** The SDK hashes documents locally, sends only the cryptographic hash to CertySign for HSM-backed signing, receives the CMS/PKCS#7 signature, and embeds it into PDF/XML/JSON — all on your system.
6
+
7
+ ---
8
+
9
+ ## Table of Contents
10
+
11
+ - [Installation](#installation)
12
+ - [Quick Start](#quick-start)
13
+ - [Architecture](#architecture)
14
+ - [Authentication](#authentication)
15
+ - [Environments](#environments)
16
+ - [API Resources](#api-resources)
17
+ - [client.sign — Hash-Based Signing](#clientsign--hash-based-signing)
18
+ - [client.sessions — Multi-Recipient Signing Sessions](#clientsessions--multi-recipient-signing-sessions)
19
+ - [client.hasher — Local Document Hashing](#clienthasher--local-document-hashing)
20
+ - [client.embedder — Local Signature Embedding](#clientembedder--local-signature-embedding)
21
+ - [client.dashboard — SDK Analytics](#clientdashboard--sdk-analytics)
22
+ - [client.certificates — X.509 Certificate Management](#clientcertificates--x509-certificate-management)
23
+ - [client.pki — PKI Infrastructure](#clientpki--pki-infrastructure)
24
+ - [client.envelopes — Envelope Management](#clientenvelopes--envelope-management)
25
+ - [client.legacySign — Legacy Document Signing](#clientlegacysign--legacy-document-signing)
26
+ - [Complete Examples](#complete-examples)
27
+ - [Error Handling](#error-handling)
28
+ - [Rate Limiting](#rate-limiting)
29
+ - [Migration from v1](#migration-from-v1)
30
+ - [PKI Trust Chain](#pki-trust-chain)
31
+ - [Security Best Practices](#security-best-practices)
32
+ - [Support](#support)
33
+
34
+ ---
4
35
 
5
36
  ## Installation
6
37
 
@@ -58,10 +89,15 @@ npm install @certysign/sdk
58
89
 
59
90
  Node.js >= 18 required.
60
91
 
61
- ## Quick start
92
+ ---
93
+
94
+ ## Quick Start
95
+
96
+ ### Sign a single document (hash-based)
62
97
 
63
98
  ```js
64
99
  const { CertySignClient } = require('@certysign/sdk');
100
+ const fs = require('fs');
65
101
 
66
102
  const client = new CertySignClient({
67
103
  publicKey: process.env.CERTYSIGN_PUBLIC_KEY, // cs_pk_...
@@ -69,19 +105,114 @@ const client = new CertySignClient({
69
105
  environment: 'production' // 'staging' | 'development'
70
106
  });
71
107
 
72
- // Sign a document
73
- const result = await client.sign.quickSign({
74
- document: require('fs').readFileSync('./claim.pdf'),
75
- filename: 'claim.pdf',
76
- signerName: 'Dr. Amina Okonkwo',
77
- signerEmail: 'amina.okonkwo@dha.go.ke',
78
- reason: 'Health claim approval — DHA Kenya'
108
+ // 1. Hash locally — document never leaves your system
109
+ const pdfBuffer = fs.readFileSync('./claim.pdf');
110
+
111
+ // 2. Hash & sign in one call (SDK hashes locally, sends only hash)
112
+ const result = await client.sign.hashAndSign({
113
+ document: pdfBuffer,
114
+ fileName: 'claim.pdf',
115
+ signerName: 'Dr. Amina Okonkwo',
116
+ reason: 'Health claim approval — DHA Kenya',
117
+ location: 'Nairobi, Kenya'
118
+ });
119
+
120
+ // 3. Embed the signature into the PDF locally
121
+ const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
122
+ signature: result.data.signature,
123
+ certificate: result.data.certificate.pem,
124
+ certSerialNumber: result.data.certificate.serialNumber,
125
+ signerName: 'Dr. Amina Okonkwo',
126
+ reason: 'Health claim approval — DHA Kenya',
127
+ documentHash: result.data.documentHash,
128
+ hashAlgorithm: result.data.hashAlgorithm,
129
+ algorithm: result.data.algorithm
79
130
  });
80
131
 
81
- console.log(result.data.envelopeId);
82
- console.log(result.data.certificate.serialNumber);
132
+ fs.writeFileSync('./claim-signed.pdf', signedPdf);
83
133
  ```
84
134
 
135
+ ### Multi-recipient signing with OTP verification
136
+
137
+ ```js
138
+ // 1. Hash documents locally
139
+ const { hash } = client.hasher.hash(pdfBuffer, 'sha256');
140
+
141
+ // 2. Create a signing session with recipients
142
+ const session = await client.sessions.create({
143
+ name: 'Q1 Financial Report Approval',
144
+ documents: [{
145
+ documentId: 'doc-q1-report',
146
+ fileName: 'Q1-report.pdf',
147
+ hash,
148
+ hashAlgorithm: 'sha256'
149
+ }],
150
+ recipients: [
151
+ { email: 'cfo@company.co.ke', name: 'CFO', role: 'signer', order: 1 },
152
+ { email: 'ceo@company.co.ke', name: 'CEO', role: 'signer', order: 2 }
153
+ ],
154
+ signingOrder: 'sequential' // CFO signs first, then CEO
155
+ });
156
+
157
+ // 3. Send OTP to recipient
158
+ await client.sessions.sendOtp(session.data.session._id, recipientId);
159
+
160
+ // 4. Verify OTP (recipient enters code from email)
161
+ const { data } = await client.sessions.verifyOtp(session.data.session._id, recipientId, '485721');
162
+
163
+ // 5. Recipient signs with their token
164
+ const signed = await client.sessions.recipientSign(
165
+ session.data.session._id,
166
+ recipientId,
167
+ data.signingToken
168
+ );
169
+ ```
170
+
171
+ ---
172
+
173
+ ## Architecture
174
+
175
+ ```
176
+ ┌─────────────────────────────────────────────────────────────────┐
177
+ │ SUBSCRIBER'S INFRASTRUCTURE │
178
+ │ │
179
+ │ ┌──────────┐ hash() ┌──────────────────┐ │
180
+ │ │ Document │─────────────▶│ DocumentHasher │ │
181
+ │ │ (PDF/XML/ │ │ (SHA-256/384/ │ │
182
+ │ │ JSON) │ │ 512) │ │
183
+ │ └──────────┘ └────────┬──────────┘ │
184
+ │ │ │ hex hash only │
185
+ │ │ ▼ │
186
+ │ │ ┌───────────────────────────────────┐ │
187
+ │ │ │ CertySign API │ │
188
+ │ │ │ POST /sdk/v1/sign/hash │ │
189
+ │ │ │ ┌─────────────────────────────┐ │ │
190
+ │ │ │ │ HSM (Google Cloud KMS) │ │ │
191
+ │ │ │ │ Signs hash with tenant key │ │ │
192
+ │ │ │ └─────────────────────────────┘ │ │
193
+ │ │ └───────────────┬───────────────────┘ │
194
+ │ │ │ CMS signature + cert + chain │
195
+ │ │ ▼ │
196
+ │ │ ┌──────────────────────┐ │
197
+ │ └─────────────▶│ SignatureEmbedder │ │
198
+ │ │ embedInPdf() │ │
199
+ │ │ embedInXml() │ │
200
+ │ │ embedInJson() │ │
201
+ │ └──────────┬───────────┘ │
202
+ │ │ │
203
+ │ ▼ │
204
+ │ ┌──────────────────────┐ │
205
+ │ │ Signed Document │ │
206
+ │ │ (stays on YOUR │ │
207
+ │ │ system) │ │
208
+ │ └──────────────────────┘ │
209
+ │ │
210
+ └─────────────────────────────────────────────────────────────────┘
211
+ ```
212
+
213
+ **What crosses the network:** Only the cryptographic hash (64-128 hex characters).
214
+ **What stays local:** The original document, the signed document, and the embedding process.
215
+
85
216
  ---
86
217
 
87
218
  ## Authentication
@@ -103,47 +234,87 @@ The secret key is shown **once** at creation. Store it in a secrets manager (Vau
103
234
 
104
235
  | Environment | Base URL |
105
236
  |-------------|----------|
106
- | `production` | `https://api.certysign.com` |
107
- | `staging` | `https://api-staging.certysign.com` |
237
+ | `production` | `https://api.certysign.io` |
238
+ | `staging` | `https://api-staging.certysign.io` |
108
239
  | `development` | `http://localhost:8000` |
109
240
 
110
241
  ---
111
242
 
112
243
  ## API Resources
113
244
 
114
- ### `client.sign` — Document Signing
245
+ ### `client.sign` — Hash-Based Signing
115
246
 
116
- #### `quickSign(options)` — Sign a PDF in one call
247
+ > **v2 (default):** Documents are hashed locally. Only the hash is sent to CertySign.
248
+ > For the legacy file-upload flow, see [`client.legacySign`](#clientlegacysign--legacy-document-signing).
249
+
250
+ #### `hashAndSign(options)` — Hash locally & sign in one call
117
251
 
118
252
  ```js
119
- const result = await client.sign.quickSign({
120
- document: Buffer, // Required — PDF buffer or ReadStream
121
- filename: 'document.pdf', // Optional — default: 'document.pdf'
122
- signerName: 'Full Name', // Required
123
- signerEmail: 'email@example.com', // Optional
124
- reason: 'Approval', // Optional
125
- location: 'Nairobi, Kenya', // Optional
126
- metadata: { claimId: '...' } // Optional — stored with envelope
253
+ const result = await client.sign.hashAndSign({
254
+ document: Buffer | string, // Required — document content
255
+ fileName: 'claim.pdf', // Optional — default: 'document.pdf'
256
+ mimeType: 'application/pdf', // Optional — auto-detected from fileName
257
+ signerName: 'Dr. Amina Okonkwo', // Optional
258
+ reason: 'Approval', // Optional
259
+ location: 'Nairobi, Kenya', // Optional
260
+ metadata: { claimId: '...' } // Optional
127
261
  });
128
- // result.data.envelopeId
262
+ // result.data.signature — base64-encoded CMS/PKCS#7 signature
263
+ // result.data.documentHash — hex hash of the document
264
+ // result.data.hashAlgorithm — 'sha256' | 'sha384' | 'sha512'
265
+ // result.data.algorithm — 'SHA256withRSA' etc.
266
+ // result.data.certificate.pem — signer certificate PEM
129
267
  // result.data.certificate.serialNumber
130
- // result.data.signedDocumentUrl
268
+ // result.data.certificate.chain — full trust chain PEM
269
+ // result.data.signedAt — ISO 8601 timestamp
270
+ ```
271
+
272
+ #### `signHash(options)` — Sign a pre-computed hash
273
+
274
+ Use this when you already computed the hash yourself.
275
+
276
+ ```js
277
+ const result = await client.sign.signHash({
278
+ documentHash: 'a3f2b8c1d4e5...', // Required — hex-encoded hash
279
+ hashAlgorithm: 'sha256', // Required — 'sha256' | 'sha384' | 'sha512'
280
+ fileName: 'report.pdf', // Optional
281
+ signerName: 'NHIF System', // Optional
282
+ reason: 'Automated signing', // Optional
283
+ location: 'Nairobi', // Optional
284
+ metadata: { batchId: '...' } // Optional
285
+ });
286
+ // Same response shape as hashAndSign
287
+ ```
288
+
289
+ #### `batchHashAndSign(options)` — Hash & sign multiple documents
290
+
291
+ ```js
292
+ const result = await client.sign.batchHashAndSign({
293
+ documents: [
294
+ { document: fs.readFileSync('./invoice-1.pdf'), fileName: 'invoice-1.pdf' },
295
+ { document: fs.readFileSync('./invoice-2.pdf'), fileName: 'invoice-2.pdf' }
296
+ ],
297
+ signerName: 'NHIF Finance System',
298
+ reason: 'Batch provider reimbursement'
299
+ });
300
+ // result.data.results[] — array of signed results
301
+ // result.data.results[].documentHash
302
+ // result.data.results[].signature
303
+ // result.data.certificate.pem — shared certificate for the batch
131
304
  ```
132
305
 
133
- #### `batchSign(options)` — Sign multiple PDFs at once
306
+ #### `batchSignHashes(options)` — Sign multiple pre-computed hashes
134
307
 
135
308
  ```js
136
- const result = await client.sign.batchSign({
309
+ const result = await client.sign.batchSignHashes({
137
310
  documents: [
138
- { data: fs.readFileSync('./invoice-1.pdf'), filename: 'invoice-1.pdf' },
139
- { data: fs.readFileSync('./invoice-2.pdf'), filename: 'invoice-2.pdf' }
311
+ { documentHash: 'a3f2...', hashAlgorithm: 'sha256', fileName: 'doc1.pdf' },
312
+ { documentHash: 'b7e1...', hashAlgorithm: 'sha256', fileName: 'doc2.pdf' }
140
313
  ],
141
- signerName: 'NHIF Finance System',
142
- signerEmail: 'invoicing@nhif.or.ke',
143
- reason: 'Batch provider reimbursement'
314
+ signerName: 'Batch System',
315
+ reason: 'Monthly invoices'
144
316
  });
145
- // result.data.results[].envelopeId
146
- // result.data.results[].success
317
+ // Up to 50 documents per batch
147
318
  ```
148
319
 
149
320
  #### `verifyById(envelopeId)` — Verify a signed envelope
@@ -155,16 +326,505 @@ const result = await client.sign.verifyById('env_abc123');
155
326
  // result.data.signedAt
156
327
  ```
157
328
 
158
- #### `verifyDocument(document, filename)` — Verify by uploading
329
+ ---
330
+
331
+ ### `client.sessions` — Multi-Recipient Signing Sessions
332
+
333
+ Signing sessions support multi-recipient workflows with OTP email verification. Documents are represented by their hashes — the actual files never leave your system.
334
+
335
+ #### Signing Order
336
+
337
+ - **`sequential`** — recipients sign in the order specified by their `order` field. Recipient 2 cannot sign until recipient 1 has completed.
338
+ - **`parallel`** — all recipients can sign independently in any order.
339
+
340
+ #### `create(options)` — Create a signing session
341
+
342
+ ```js
343
+ const session = await client.sessions.create({
344
+ name: 'Q1 Board Resolution', // Required
345
+ documents: [ // Required — at least one
346
+ {
347
+ documentId: 'doc-resolution', // Required — your unique ID
348
+ fileName: 'board-resolution.pdf', // Required
349
+ hash: 'a3f2b8c1d4e5f6...', // Required — hex hash
350
+ hashAlgorithm: 'sha256', // Required
351
+ mimeType: 'application/pdf' // Optional
352
+ }
353
+ ],
354
+ recipients: [ // Required — at least one
355
+ {
356
+ email: 'chair@board.co.ke',
357
+ name: 'Board Chair',
358
+ role: 'signer', // 'signer' | 'viewer' | 'approver'
359
+ order: 1
360
+ },
361
+ {
362
+ email: 'secretary@board.co.ke',
363
+ name: 'Company Secretary',
364
+ role: 'signer',
365
+ order: 2
366
+ }
367
+ ],
368
+ signingOrder: 'sequential', // 'sequential' | 'parallel'
369
+ expiresAt: '2026-02-01T00:00:00Z' // Optional — default: 7 days
370
+ });
371
+ // session.data.session._id
372
+ // session.data.session.status — 'active'
373
+ // session.data.session.recipients[] — each has .recipientId
374
+ ```
375
+
376
+ #### `get(sessionId)` — Get session details
377
+
378
+ ```js
379
+ const { data } = await client.sessions.get(sessionId);
380
+ // data.session.status — 'active' | 'completed' | 'expired'
381
+ // data.session.documents[] — includes .signature after signing
382
+ // data.session.recipients[].status — 'pending' | 'otp_sent' | 'verified' | 'signed'
383
+ ```
384
+
385
+ #### `list(options)` — List signing sessions
159
386
 
160
387
  ```js
161
- const result = await client.sign.verifyDocument(fs.readFileSync('./signed.pdf'));
388
+ const { data } = await client.sessions.list({
389
+ page: 1,
390
+ limit: 20,
391
+ status: 'active' // Optional filter
392
+ });
393
+ // data.sessions[]
394
+ // data.pagination.total
395
+ ```
396
+
397
+ #### `sendOtp(sessionId, recipientId)` — Send OTP email
398
+
399
+ Sends a 6-digit OTP code to the recipient's email address. The OTP is valid for 10 minutes. Respects signing order — in sequential mode, you cannot send OTP to recipient 2 until recipient 1 has signed.
400
+
401
+ ```js
402
+ await client.sessions.sendOtp(sessionId, recipientId);
403
+ // OTP email sent to recipient
404
+ ```
405
+
406
+ #### `verifyOtp(sessionId, recipientId, code)` — Verify OTP
407
+
408
+ Validates the OTP code. After 5 failed attempts, the OTP is locked and must be re-sent. Returns a signing token valid for 30 minutes.
409
+
410
+ ```js
411
+ const { data } = await client.sessions.verifyOtp(sessionId, recipientId, '485721');
412
+ // data.signingToken — use this for the signing step
413
+ // data.expiresAt — token expiry (30 minutes)
414
+ ```
415
+
416
+ #### `recipientSign(sessionId, recipientId, signingToken)` — Recipient signs
417
+
418
+ Signs all document hashes in the session using the tenant's HSM-backed certificate. The signing token must be valid and unexpired.
419
+
420
+ ```js
421
+ const { data } = await client.sessions.recipientSign(sessionId, recipientId, signingToken);
422
+ // data.session.status — 'completed' if all recipients have signed
423
+ // data.session.documents[].signature — base64 CMS signature for each document
424
+ // data.session.documents[].signedAt
425
+ // data.session.documents[].signedBy
426
+ ```
427
+
428
+ #### Full Signing Session Flow
429
+
430
+ ```js
431
+ const fs = require('fs');
432
+ const { CertySignClient, DocumentHasher } = require('@certysign/sdk');
433
+
434
+ const client = new CertySignClient({ publicKey, secretKey });
435
+ const hasher = new DocumentHasher();
436
+
437
+ // 1. Hash documents locally
438
+ const doc1 = fs.readFileSync('./contract.pdf');
439
+ const doc2 = fs.readFileSync('./addendum.pdf');
440
+ const hash1 = hasher.hash(doc1, 'sha256');
441
+ const hash2 = hasher.hash(doc2, 'sha256');
442
+
443
+ // 2. Create session
444
+ const { data: { session } } = await client.sessions.create({
445
+ name: 'Service Contract Signing',
446
+ documents: [
447
+ { documentId: 'contract', fileName: 'contract.pdf', hash: hash1.hash, hashAlgorithm: 'sha256' },
448
+ { documentId: 'addendum', fileName: 'addendum.pdf', hash: hash2.hash, hashAlgorithm: 'sha256' }
449
+ ],
450
+ recipients: [
451
+ { email: 'vendor@example.com', name: 'Vendor Rep', role: 'signer', order: 1 },
452
+ { email: 'client@example.com', name: 'Client Rep', role: 'signer', order: 2 }
453
+ ],
454
+ signingOrder: 'sequential'
455
+ });
456
+
457
+ // 3. First recipient: send OTP → verify → sign
458
+ const recipientId = session.recipients[0].recipientId;
459
+ await client.sessions.sendOtp(session._id, recipientId);
460
+
461
+ // (Recipient enters code from their email)
462
+ const { data: { signingToken } } = await client.sessions.verifyOtp(
463
+ session._id, recipientId, '123456'
464
+ );
465
+ await client.sessions.recipientSign(session._id, recipientId, signingToken);
466
+
467
+ // 4. Second recipient follows the same flow...
468
+
469
+ // 5. Retrieve completed session and embed signatures
470
+ const completed = await client.sessions.get(session._id);
471
+ for (const doc of completed.data.session.documents) {
472
+ const originalBuffer = doc.documentId === 'contract' ? doc1 : doc2;
473
+ const signed = await client.embedder.embedInPdf(originalBuffer, {
474
+ signature: doc.signature,
475
+ certificate: completed.data.session.certSerialNumber,
476
+ documentHash: doc.hash,
477
+ hashAlgorithm: doc.hashAlgorithm
478
+ });
479
+ fs.writeFileSync(`./signed-${doc.fileName}`, signed);
480
+ }
481
+ ```
482
+
483
+ ---
484
+
485
+ ### `client.hasher` — Local Document Hashing
486
+
487
+ All hashing is done **locally** on your machine. No data is sent to CertySign.
488
+
489
+ #### `hash(data, algorithm)` — Hash a buffer or string
490
+
491
+ ```js
492
+ const { hash, algorithm, size } = client.hasher.hash(
493
+ fs.readFileSync('./document.pdf'),
494
+ 'sha256' // Optional — default: 'sha256'. Also: 'sha384', 'sha512'
495
+ );
496
+ // hash — hex-encoded hash string
497
+ // algorithm — 'sha256'
498
+ // size — input size in bytes
499
+ ```
500
+
501
+ #### `hashFile(filePath, algorithm)` — Hash a file by path (streaming)
502
+
503
+ Memory-efficient for large files — reads with streaming instead of loading the entire file.
504
+
505
+ ```js
506
+ const { hash, algorithm, size } = await client.hasher.hashFile(
507
+ '/path/to/large-report.pdf',
508
+ 'sha256'
509
+ );
510
+ ```
511
+
512
+ #### `hashMany(documents, algorithm)` — Batch hash buffers
513
+
514
+ ```js
515
+ const results = client.hasher.hashMany([
516
+ { data: fs.readFileSync('./doc1.pdf'), fileName: 'doc1.pdf' },
517
+ { data: fs.readFileSync('./doc2.pdf'), fileName: 'doc2.pdf' }
518
+ ], 'sha256');
519
+ // results[] — each: { fileName, hash, algorithm, size }
520
+ ```
521
+
522
+ #### `hashFiles(filePaths, algorithm)` — Batch hash file paths
523
+
524
+ ```js
525
+ const results = await client.hasher.hashFiles([
526
+ '/path/to/doc1.pdf',
527
+ '/path/to/doc2.pdf'
528
+ ], 'sha256');
529
+ // results[] — each: { filePath, fileName, hash, algorithm, size }
530
+ ```
531
+
532
+ **Supported algorithms:** `sha256`, `sha384`, `sha512`
533
+
534
+ ---
535
+
536
+ ### `client.embedder` — Local Signature Embedding
537
+
538
+ Embeds CMS/PKCS#7 signatures into documents **locally** on your machine. No data is sent to CertySign during embedding.
539
+
540
+ #### `embedInPdf(pdfBuffer, options)` — Embed signature into PDF
541
+
542
+ Creates a visual signature stamp matching the CertySign platform and embeds CMS metadata into the PDF. Supports **multi-recipient** signing — each recipient gets their own visual stamp, stacked from the bottom of the page upward.
543
+
544
+ **Single signer:**
545
+
546
+ ```js
547
+ const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
548
+ signature: 'base64-cms-signature', // Required
549
+ signerEmail: 'ndolimathews@gmail.com', // Recommended — shown on stamp
550
+ signerName: 'Mathews Ndoli', // Fallback if no email
551
+ certSerialNumber: '9EF9C8E88478FC08C6759942274EB8A2', // Optional
552
+ certificate: '-----BEGIN CERTIFICATE-----...', // Optional — PEM
553
+ chain: '-----BEGIN CERTIFICATE-----...', // Optional — chain PEM
554
+ documentHash: 'a3f2b8c1...', // Optional — hex hash
555
+ hashAlgorithm: 'sha256', // Optional
556
+ algorithm: 'SHA256withRSA', // Optional
557
+ });
558
+ // Returns: Buffer — signed PDF
559
+ fs.writeFileSync('./signed.pdf', signedPdf);
560
+ ```
561
+
562
+ **Multi-recipient (2+ signers):**
563
+
564
+ ```js
565
+ const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
566
+ signatures: [
567
+ {
568
+ signature: 'base64-cms-sig-cfo',
569
+ recipientEmail: 'cfo@company.co.ke',
570
+ recipientName: 'Jane Mwangi',
571
+ certSerialNumber: 'A1B2C3D4E5F6...',
572
+ signatureAlgorithm: 'SHA256withRSA',
573
+ signedAt: '2026-03-13T11:07:28.927Z'
574
+ },
575
+ {
576
+ signature: 'base64-cms-sig-ceo',
577
+ recipientEmail: 'ceo@company.co.ke',
578
+ recipientName: 'James Otieno',
579
+ certSerialNumber: 'F6E5D4C3B2A1...',
580
+ signatureAlgorithm: 'SHA256withRSA',
581
+ signedAt: '2026-03-13T11:12:45.000Z'
582
+ }
583
+ ],
584
+ documentHash: 'a3f2b8c1...',
585
+ hashAlgorithm: 'sha256'
586
+ });
587
+ ```
588
+
589
+ Each signer gets a visual stamp matching the CertySign platform format:
590
+
591
+ ```
592
+ ┌──────────────────────────────────────┐
593
+ │ Digitally signed by: cfo@company.co.ke│
594
+ │ Date: 2026-03-13T11:07:28.927Z │
595
+ │ Certificate: A1B2C3D4E5F6... │
596
+ │ Standard: PAdES Baseline B-B │
597
+ └──────────────────────────────────────┘
598
+ ┌──────────────────────────────────────┐
599
+ │ Digitally signed by: ceo@company.co.ke│
600
+ │ Date: 2026-03-13T11:12:45.000Z │
601
+ │ Certificate: F6E5D4C3B2A1... │
602
+ │ Standard: PAdES Baseline B-B │
603
+ └──────────────────────────────────────┘
604
+ ```
605
+
606
+ Multiple stamps auto-stack from the bottom-left of each page upward. PDF metadata includes per-signer entries (`CertySign-Signature-0`, `CertySign-Signer-0`, `CertySign-CertSerial-0`, etc.).
607
+
608
+ #### `embedInXml(xmlString, options)` — Embed XMLDSig signature
609
+
610
+ Creates W3C XML Digital Signature (XMLDSig) enveloped signatures. Supports multi-signer — each signer gets a separate `<ds:Signature>` element.
611
+
612
+ ```js
613
+ // Single signer
614
+ const signedXml = client.embedder.embedInXml(xmlString, {
615
+ signature: 'base64-cms-signature',
616
+ certificate: '-----BEGIN CERTIFICATE-----...',
617
+ certSerialNumber: 'A1B2C3...',
618
+ signerEmail: 'signer@company.co.ke',
619
+ signerName: 'System',
620
+ documentHash: 'a3f2b8c1...',
621
+ hashAlgorithm: 'sha256',
622
+ algorithm: 'SHA256withRSA'
623
+ });
624
+
625
+ // Multi-signer
626
+ const signedXml = client.embedder.embedInXml(xmlString, {
627
+ signatures: [
628
+ { signature: 'sig1', recipientEmail: 'cfo@co.ke', certSerialNumber: 'A1...' },
629
+ { signature: 'sig2', recipientEmail: 'ceo@co.ke', certSerialNumber: 'B2...' }
630
+ ],
631
+ certificate: '-----BEGIN CERTIFICATE-----...',
632
+ documentHash: 'a3f2b8c1...',
633
+ hashAlgorithm: 'sha256',
634
+ algorithm: 'SHA256withRSA'
635
+ });
636
+ // Returns: string — XML with <ds:Signature> elements
637
+ ```
638
+
639
+ Each `<ds:Signature>` element includes signer-specific properties:
640
+
641
+ ```xml
642
+ <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Id="CertySign-Signature-0">
643
+ <ds:SignedInfo>
644
+ <ds:CanonicalizationMethod Algorithm="...xml-c14n11#"/>
645
+ <ds:SignatureMethod Algorithm="...rsa-sha256"/>
646
+ <ds:Reference URI="">
647
+ <ds:DigestMethod Algorithm="...sha256"/>
648
+ <ds:DigestValue>...</ds:DigestValue>
649
+ </ds:Reference>
650
+ </ds:SignedInfo>
651
+ <ds:SignatureValue>...</ds:SignatureValue>
652
+ <ds:KeyInfo>
653
+ <ds:X509Data>
654
+ <ds:X509Certificate>...</ds:X509Certificate>
655
+ </ds:X509Data>
656
+ </ds:KeyInfo>
657
+ <ds:Object>
658
+ <ds:SignatureProperties>
659
+ <ds:SignatureProperty>
660
+ <SignerEmail>cfo@co.ke</SignerEmail>
661
+ <CertSerial>A1...</CertSerial>
662
+ <Standard>PAdES Baseline B-B</Standard>
663
+ </ds:SignatureProperty>
664
+ </ds:SignatureProperties>
665
+ </ds:Object>
666
+ </ds:Signature>
667
+ ```
668
+
669
+ #### `embedInJson(jsonData, options)` — Create a signed JSON envelope
670
+
671
+ Creates a signed JSON envelope with a `signatures[]` array. Supports multi-signer.
672
+
673
+ ```js
674
+ // Single signer
675
+ const signedJson = client.embedder.embedInJson(jsonData, {
676
+ signature: 'base64-cms-signature',
677
+ certificate: '-----BEGIN CERTIFICATE-----...',
678
+ certSerialNumber: 'A1B2C3...',
679
+ signerEmail: 'signer@company.co.ke',
680
+ signerName: 'API System',
681
+ documentHash: 'a3f2b8c1...',
682
+ hashAlgorithm: 'sha256',
683
+ algorithm: 'SHA256withRSA'
684
+ });
685
+
686
+ // Multi-signer
687
+ const signedJson = client.embedder.embedInJson(jsonData, {
688
+ signatures: [
689
+ { signature: 'sig1', recipientEmail: 'cfo@co.ke', recipientName: 'Jane', certSerialNumber: 'A1...' },
690
+ { signature: 'sig2', recipientEmail: 'ceo@co.ke', recipientName: 'James', certSerialNumber: 'B2...' }
691
+ ],
692
+ documentHash: 'a3f2b8c1...',
693
+ hashAlgorithm: 'sha256',
694
+ algorithm: 'SHA256withRSA'
695
+ });
696
+ // Returns: object
697
+ ```
698
+
699
+ Output structure (v2 — **breaking change** from v1's single `signature` object):
700
+
701
+ ```json
702
+ {
703
+ "data": { /* original JSON data */ },
704
+ "signatures": [
705
+ {
706
+ "value": "base64-cms-sig-1",
707
+ "algorithm": "SHA256withRSA",
708
+ "certificate": "-----BEGIN CERTIFICATE-----...",
709
+ "signer": {
710
+ "email": "cfo@co.ke",
711
+ "name": "Jane",
712
+ "certSerialNumber": "A1...",
713
+ "signedAt": "2026-03-13T11:07:28.927Z"
714
+ },
715
+ "standard": "PAdES Baseline B-B",
716
+ "digest": { "hash": "a3f2b8c1...", "algorithm": "sha256" }
717
+ },
718
+ {
719
+ "value": "base64-cms-sig-2",
720
+ "algorithm": "SHA256withRSA",
721
+ "certificate": "-----BEGIN CERTIFICATE-----...",
722
+ "signer": {
723
+ "email": "ceo@co.ke",
724
+ "name": "James",
725
+ "certSerialNumber": "B2...",
726
+ "signedAt": "2026-03-13T11:12:45.000Z"
727
+ },
728
+ "standard": "PAdES Baseline B-B",
729
+ "digest": { "hash": "a3f2b8c1...", "algorithm": "sha256" }
730
+ }
731
+ ],
732
+ "metadata": {
733
+ "provider": "CertySign Trust Services",
734
+ "version": "2.0.0",
735
+ "signatureCount": 2,
736
+ "signedAt": "2026-03-13T11:12:45.000Z"
737
+ }
738
+ }
739
+ ```
740
+
741
+ ---
742
+
743
+ ### `client.dashboard` — SDK Analytics
744
+
745
+ Track documents signed via the SDK, unique recipients, success/failure rates, and daily trends.
746
+
747
+ #### `getStats(options?)` — Aggregate signing statistics
748
+
749
+ ```js
750
+ const { data } = await client.dashboard.getStats({
751
+ from: '2026-01-01', // Optional — ISO date
752
+ to: '2026-03-31' // Optional — ISO date
753
+ });
754
+
755
+ // data.documentsSignedTotal — total documents signed across all sessions
756
+ // data.documentsSignedToday — documents signed today
757
+ // data.documentsSignedThisMonth — documents signed this month
758
+ // data.signingSessionsTotal — total signing sessions created
759
+ // data.signingSessionsByStatus — { draft, active, completed, expired, cancelled }
760
+ // data.successRate — percentage of completed sessions
761
+ // data.uniqueRecipientsCount — unique recipients (deduplicated by email)
762
+ // data.recipientsList — top recipients sorted by signCount desc
763
+ // data.dailyTrend — last 30 days: [{ date, documentsSigned }]
764
+ // data.dateRange — { from, to } applied filter
765
+ ```
766
+
767
+ #### `getRecipients(options?)` — Unique recipient list with activity
768
+
769
+ ```js
770
+ const { data } = await client.dashboard.getRecipients({
771
+ page: 1, // Default: 1
772
+ limit: 20, // Default: 20
773
+ search: 'company.co' // Optional — filter by name or email
774
+ });
775
+
776
+ // data.recipients[] — each entry:
777
+ // .email — unique email address
778
+ // .name — recipient name
779
+ // .totalSessions — sessions this recipient was part of
780
+ // .signedSessions — sessions where they signed
781
+ // .pendingSessions — sessions still pending their signature
782
+ // .declinedSessions — sessions they declined
783
+ // .lastActivity — last interaction timestamp
784
+ // .firstSeen — first time they appeared
785
+ // data.pagination — { page, limit, total, pages }
786
+ ```
787
+
788
+ #### `getDocuments(options?)` — Document-level signing details
789
+
790
+ ```js
791
+ const { data } = await client.dashboard.getDocuments({
792
+ page: 1,
793
+ limit: 20,
794
+ status: 'completed' // Optional — filter by session status
795
+ });
796
+
797
+ // data.documents[] — each entry:
798
+ // .sessionId — parent signing session ID
799
+ // .sessionName — session name
800
+ // .fileName — document file name
801
+ // .hash — document hash
802
+ // .signatures[] — per-recipient signatures with email, certSerial, signedAt
803
+ // .signatureCount — number of signatures on this document
804
+ // .recipientCount — total recipients in the session
805
+ // data.pagination — { page, limit, total, pages }
162
806
  ```
163
807
 
164
808
  ---
165
809
 
166
810
  ### `client.certificates` — X.509 Certificate Management
167
811
 
812
+ #### `getActive()` — Get your tenant's active signing certificate
813
+
814
+ Returns the HSM-backed certificate used for hash signing operations.
815
+
816
+ ```js
817
+ const { data } = await client.certificates.getActive();
818
+ // data.certificate.serialNumber
819
+ // data.certificate.fingerprint
820
+ // data.certificate.pem — PEM certificate
821
+ // data.certificate.chain — full trust chain PEM
822
+ // data.certificate.validFrom
823
+ // data.certificate.validUntil
824
+ // data.certificate.subject — { commonName, organization, ... }
825
+ // data.certificate.trustSettings — signing capabilities
826
+ ```
827
+
168
828
  #### `issue(options)` — Issue a per-document certificate
169
829
 
170
830
  ```js
@@ -188,9 +848,9 @@ const result = await client.certificates.issue({
188
848
  ```
189
849
 
190
850
  Issued certificates include:
191
- - **CRL Distribution Point** → `https://pki.certysign.com/crl/intermediate.crl`
192
- - **OCSP (AIA)** → `https://pki.certysign.com/ocsp`
193
- - **CA Issuers (AIA)** → `https://pki.certysign.com/certs/intermediate.crt`
851
+ - **CRL Distribution Point** → `https://pki.certysign.io/crl/intermediate.crl`
852
+ - **OCSP (AIA)** → `https://pki.certysign.io/ocsp`
853
+ - **CA Issuers (AIA)** → `https://pki.certysign.io/certs/intermediate.crt`
194
854
 
195
855
  #### `verify(serialNumber, atTime?)` — Verify certificate validity
196
856
 
@@ -275,7 +935,7 @@ const { data } = await client.pki.info();
275
935
 
276
936
  ### `client.envelopes` — Envelope Management
277
937
 
278
- For integrations that need full control over the envelope lifecycle (instead of `quickSign`).
938
+ For integrations that need full control over the envelope lifecycle.
279
939
 
280
940
  ```js
281
941
  // 1. Create envelope
@@ -309,6 +969,235 @@ const { data } = await client.envelopes.getAuditTrail(envelope._id);
309
969
 
310
970
  ---
311
971
 
972
+ ### `client.legacySign` — Legacy Document Signing
973
+
974
+ > **Deprecated in v2.** Uploads entire documents to CertySign for server-side signing. Use [`client.sign`](#clientsign--hash-based-signing) instead for hash-based signing where documents never leave your system.
975
+
976
+ ```js
977
+ // Legacy quickSign (documents are uploaded to CertySign)
978
+ const result = await client.legacySign.quickSign({
979
+ document: fs.readFileSync('./claim.pdf'),
980
+ filename: 'claim.pdf',
981
+ signerName: 'Dr. Amina Okonkwo',
982
+ signerEmail: 'amina.okonkwo@dha.go.ke',
983
+ reason: 'Health claim approval'
984
+ });
985
+ ```
986
+
987
+ Legacy methods: `quickSign()`, `batchSign()`, `verifyById()`, `verifyDocument()`.
988
+
989
+ ---
990
+
991
+ ## Complete Examples
992
+
993
+ ### PDF: Hash → Sign → Embed
994
+
995
+ ```js
996
+ const fs = require('fs');
997
+ const { CertySignClient } = require('@certysign/sdk');
998
+
999
+ const client = new CertySignClient({
1000
+ publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
1001
+ secretKey: process.env.CERTYSIGN_SECRET_KEY
1002
+ });
1003
+
1004
+ async function signPdf(filePath) {
1005
+ const pdf = fs.readFileSync(filePath);
1006
+
1007
+ // Hash & sign — document stays local
1008
+ const { data } = await client.sign.hashAndSign({
1009
+ document: pdf,
1010
+ fileName: 'contract.pdf',
1011
+ signerName: 'Legal Department',
1012
+ reason: 'Contract execution'
1013
+ });
1014
+
1015
+ // Embed signature into PDF locally
1016
+ const signed = await client.embedder.embedInPdf(pdf, {
1017
+ signature: data.signature,
1018
+ certificate: data.certificate.pem,
1019
+ certSerialNumber: data.certificate.serialNumber,
1020
+ chain: data.certificate.chain,
1021
+ signerName: 'Legal Department',
1022
+ reason: 'Contract execution',
1023
+ documentHash: data.documentHash,
1024
+ hashAlgorithm: data.hashAlgorithm,
1025
+ algorithm: data.algorithm
1026
+ });
1027
+
1028
+ fs.writeFileSync(filePath.replace('.pdf', '-signed.pdf'), signed);
1029
+ console.log('Signed:', data.certificate.serialNumber);
1030
+ }
1031
+ ```
1032
+
1033
+ ### XML: Hash → Sign → Embed XMLDSig
1034
+
1035
+ ```js
1036
+ async function signXml(filePath) {
1037
+ const xml = fs.readFileSync(filePath, 'utf-8');
1038
+ const { hash, algorithm } = client.hasher.hash(Buffer.from(xml), 'sha256');
1039
+
1040
+ const { data } = await client.sign.signHash({
1041
+ documentHash: hash,
1042
+ hashAlgorithm: algorithm,
1043
+ fileName: 'invoice.xml'
1044
+ });
1045
+
1046
+ const signedXml = client.embedder.embedInXml(xml, {
1047
+ signature: data.signature,
1048
+ certificate: data.certificate.pem,
1049
+ certSerialNumber: data.certificate.serialNumber,
1050
+ documentHash: hash,
1051
+ hashAlgorithm: algorithm,
1052
+ algorithm: data.algorithm
1053
+ });
1054
+
1055
+ fs.writeFileSync(filePath.replace('.xml', '-signed.xml'), signedXml);
1056
+ }
1057
+ ```
1058
+
1059
+ ### JSON: Hash → Sign → Envelope
1060
+
1061
+ ```js
1062
+ async function signJson(data) {
1063
+ const json = JSON.stringify(data);
1064
+ const { hash, algorithm } = client.hasher.hash(Buffer.from(json), 'sha256');
1065
+
1066
+ const { data: sig } = await client.sign.signHash({
1067
+ documentHash: hash,
1068
+ hashAlgorithm: algorithm,
1069
+ fileName: 'payload.json'
1070
+ });
1071
+
1072
+ return client.embedder.embedInJson(data, {
1073
+ signature: sig.signature,
1074
+ certificate: sig.certificate.pem,
1075
+ certSerialNumber: sig.certificate.serialNumber,
1076
+ documentHash: hash,
1077
+ hashAlgorithm: algorithm,
1078
+ algorithm: sig.algorithm
1079
+ });
1080
+ }
1081
+ ```
1082
+
1083
+ ### Batch Signing (50 invoices)
1084
+
1085
+ ```js
1086
+ const invoiceDir = './invoices/pending/';
1087
+ const files = fs.readdirSync(invoiceDir).filter(f => f.endsWith('.pdf'));
1088
+
1089
+ // Hash all locally
1090
+ const documents = files.map(f => ({
1091
+ document: fs.readFileSync(`${invoiceDir}${f}`),
1092
+ fileName: f
1093
+ }));
1094
+
1095
+ // One API call for all 50
1096
+ const { data } = await client.sign.batchHashAndSign({
1097
+ documents,
1098
+ signerName: 'NHIF Finance System',
1099
+ reason: 'Monthly provider reimbursement'
1100
+ });
1101
+
1102
+ // Embed signatures locally
1103
+ for (let i = 0; i < data.results.length; i++) {
1104
+ const signed = await client.embedder.embedInPdf(documents[i].document, {
1105
+ signature: data.results[i].signature,
1106
+ certificate: data.certificate.pem,
1107
+ certSerialNumber: data.certificate.serialNumber,
1108
+ documentHash: data.results[i].documentHash,
1109
+ hashAlgorithm: data.results[i].hashAlgorithm,
1110
+ algorithm: data.results[i].algorithm
1111
+ });
1112
+ fs.writeFileSync(`./invoices/signed/${files[i]}`, signed);
1113
+ }
1114
+ ```
1115
+
1116
+ ### Multi-Recipient Signing Session → Embed
1117
+
1118
+ ```js
1119
+ const fs = require('fs');
1120
+ const { CertySignClient } = require('@certysign/sdk');
1121
+
1122
+ const client = new CertySignClient({
1123
+ publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
1124
+ secretKey: process.env.CERTYSIGN_SECRET_KEY
1125
+ });
1126
+
1127
+ async function multiRecipientSign() {
1128
+ const pdf = fs.readFileSync('./board-resolution.pdf');
1129
+ const { hash, algorithm } = client.hasher.hash(pdf, 'sha256');
1130
+
1131
+ // 1. Create signing session with multiple recipients
1132
+ const { data: session } = await client.sessions.create({
1133
+ name: 'Board Resolution Q1 2026',
1134
+ documents: [{ fileName: 'board-resolution.pdf', hash, hashAlgorithm: algorithm }],
1135
+ recipients: [
1136
+ { name: 'Jane Mwangi', email: 'cfo@company.co.ke', role: 'signer' },
1137
+ { name: 'James Otieno', email: 'ceo@company.co.ke', role: 'signer' }
1138
+ ],
1139
+ signingOrder: 'sequential', // or 'parallel'
1140
+ expiresIn: '7d'
1141
+ });
1142
+
1143
+ // 2. Each recipient signs after OTP verification
1144
+ // (Normally triggered from the recipient's signing link)
1145
+ // After all recipients sign, session status becomes 'completed'
1146
+
1147
+ // 3. Retrieve completed session with all signatures
1148
+ const { data: completed } = await client.sessions.get(session.sessionId);
1149
+
1150
+ // 4. Embed all signatures into the PDF
1151
+ const doc = completed.documents[0];
1152
+ const signedPdf = await client.embedder.embedInPdf(pdf, {
1153
+ signatures: doc.signatures, // Array of per-recipient signatures
1154
+ documentHash: doc.hash,
1155
+ hashAlgorithm: doc.hashAlgorithm
1156
+ });
1157
+
1158
+ fs.writeFileSync('./board-resolution-signed.pdf', signedPdf);
1159
+ // PDF now has stacked visual stamps for each signer
1160
+ }
1161
+ ```
1162
+
1163
+ ### Dashboard: Track SDK Signing Activity
1164
+
1165
+ ```js
1166
+ async function showDashboard() {
1167
+ // Overall stats
1168
+ const { data: stats } = await client.dashboard.getStats({
1169
+ from: '2026-01-01',
1170
+ to: '2026-03-31'
1171
+ });
1172
+ console.log(`Total documents signed: ${stats.documentsSignedTotal}`);
1173
+ console.log(`Success rate: ${stats.successRate}%`);
1174
+ console.log(`Unique recipients: ${stats.uniqueRecipientsCount}`);
1175
+
1176
+ // Daily trend chart data
1177
+ stats.dailyTrend.forEach(d => {
1178
+ console.log(` ${d.date}: ${d.documentsSigned} documents`);
1179
+ });
1180
+
1181
+ // Paginated recipient list
1182
+ const { data: recipients } = await client.dashboard.getRecipients({
1183
+ page: 1, limit: 10, search: 'company.co'
1184
+ });
1185
+ recipients.recipients.forEach(r => {
1186
+ console.log(`${r.email}: ${r.signedSessions} signed, ${r.pendingSessions} pending`);
1187
+ });
1188
+
1189
+ // Document-level details
1190
+ const { data: docs } = await client.dashboard.getDocuments({
1191
+ status: 'completed', page: 1, limit: 10
1192
+ });
1193
+ docs.documents.forEach(d => {
1194
+ console.log(`${d.fileName}: ${d.signatureCount} signatures`);
1195
+ });
1196
+ }
1197
+ ```
1198
+
1199
+ ---
1200
+
312
1201
  ## Error Handling
313
1202
 
314
1203
  All SDK methods throw `CertySignError` on failure:
@@ -317,7 +1206,7 @@ All SDK methods throw `CertySignError` on failure:
317
1206
  const { CertySignClient, CertySignError } = require('@certysign/sdk');
318
1207
 
319
1208
  try {
320
- await client.sign.quickSign({ ... });
1209
+ await client.sign.hashAndSign({ document: pdfBuffer });
321
1210
  } catch (err) {
322
1211
  if (err instanceof CertySignError) {
323
1212
  console.error(err.message); // Human-readable message
@@ -337,6 +1226,15 @@ Common error codes:
337
1226
  | `INSUFFICIENT_SDK_PERMISSIONS` | 403 | Key lacks required permission |
338
1227
  | `IP_NOT_ALLOWED` | 403 | Request IP not in IP allowlist |
339
1228
  | `RATE_LIMIT_EXCEEDED` | 429 | Requests per minute exceeded |
1229
+ | `NO_ACTIVE_CERTIFICATE` | 404 | Tenant has no active signing certificate |
1230
+ | `CERTIFICATE_REVOKED` | 400 | Active certificate has been revoked |
1231
+ | `INVALID_HASH` | 400 | Hash format is invalid (must be hex) |
1232
+ | `INVALID_OTP` | 400 | OTP code is incorrect |
1233
+ | `OTP_EXPIRED` | 400 | OTP has expired (10 minute window) |
1234
+ | `OTP_MAX_ATTEMPTS` | 400 | Too many failed OTP attempts (max 5) |
1235
+ | `SIGNING_TOKEN_EXPIRED` | 400 | Signing token has expired (30 minute window) |
1236
+ | `SESSION_EXPIRED` | 400 | Signing session has expired |
1237
+ | `SIGNING_ORDER_VIOLATION` | 400 | Previous recipient hasn't signed yet (sequential mode) |
340
1238
  | `NETWORK_ERROR` | 0 | Could not reach the API |
341
1239
 
342
1240
  The SDK automatically retries `429`, `502`, `503`, `504` responses up to 3 times with exponential back-off.
@@ -352,17 +1250,54 @@ Each API key has a configurable rate limit (requests per minute, set in the Cert
352
1250
  - SDK retries automatically (up to 3 times) if the issue is transient
353
1251
  - After retries are exhausted, `CertySignError` with code `RATE_LIMIT_EXCEEDED` is thrown
354
1252
 
355
- For batch workloads, use `batchSign()` to sign multiple documents in a single API call rather than calling `quickSign()` in a loop.
1253
+ For batch workloads, use `batchHashAndSign()` or `batchSignHashes()` to sign up to 50 documents in a single API call.
356
1254
 
357
1255
  ---
358
1256
 
359
- ## Examples
360
-
361
- | File | Description |
362
- |------|-------------|
363
- | [examples/dha-integration.js](examples/dha-integration.js) | DHA Kenya claims quick-sign + certificate issuance |
364
- | [examples/nhif-batch-sign.js](examples/nhif-batch-sign.js) | NHIF nightly invoice batch signing |
365
- | [examples/certificate-flow.js](examples/certificate-flow.js) | Full X.509 lifecycle: issue, verify, CRL, OCSP, chain |
1257
+ ## Migration from v1
1258
+
1259
+ ### Breaking Changes in v2.0.0
1260
+
1261
+ | v1 | v2 | Notes |
1262
+ |----|-----|-------|
1263
+ | `client.sign.quickSign()` | `client.sign.hashAndSign()` | Documents no longer uploaded. Hash locally, sign remotely. |
1264
+ | `client.sign.batchSign()` | `client.sign.batchHashAndSign()` | Same principle — only hashes cross the network. |
1265
+ | `client.sign` (SigningResource) | `client.legacySign` | Old file-upload signing moved to `legacySign` |
1266
+ | — | `client.sign` (HashSigningResource) | New hash-based signing is now `client.sign` |
1267
+ | — | `client.sessions` | New multi-recipient signing sessions with OTP |
1268
+ | — | `client.hasher` | New local document hashing utility |
1269
+ | — | `client.embedder` | New local signature embedding (PDF/XML/JSON) |
1270
+ | — | `client.dashboard` | New SDK analytics (stats, recipients, documents) |
1271
+ | — | `client.certificates.getActive()` | New active cert retrieval |
1272
+ | `embedInJson` → `signature: {}` | `embedInJson` → `signatures: []` | JSON output changed from single object to array |
1273
+
1274
+ ### Migration Steps
1275
+
1276
+ 1. **Replace `quickSign`** with `hashAndSign` + `embedInPdf`:
1277
+ ```js
1278
+ // v1 — document uploaded to CertySign
1279
+ await client.sign.quickSign({ document: pdf, filename: 'doc.pdf', signerName: '...' });
1280
+
1281
+ // v2 — document stays local
1282
+ const result = await client.sign.hashAndSign({ document: pdf, fileName: 'doc.pdf' });
1283
+ const signed = await client.embedder.embedInPdf(pdf, { signature: result.data.signature, ... });
1284
+ ```
1285
+
1286
+ 2. **Replace `batchSign`** with `batchHashAndSign`:
1287
+ ```js
1288
+ // v1
1289
+ await client.sign.batchSign({ documents: [...] });
1290
+
1291
+ // v2
1292
+ const result = await client.sign.batchHashAndSign({ documents: [...] });
1293
+ // Then embed each signature locally
1294
+ ```
1295
+
1296
+ 3. **Use `client.legacySign`** if you need the old behavior temporarily:
1297
+ ```js
1298
+ // Still works — but deprecated
1299
+ await client.legacySign.quickSign({ ... });
1300
+ ```
366
1301
 
367
1302
  ---
368
1303
 
@@ -372,27 +1307,31 @@ For batch workloads, use `batchSign()` to sign multiple documents in a single AP
372
1307
  CertySign Root CA (C=KE, RSA-4096, 20yr)
373
1308
  └── CertySign Intermediate CA (C=KE, RSA-4096, 10yr, pathLen=0)
374
1309
  ├── Per-document X.509 certs (issued via /sdk/v1/certificates/issue)
375
- └── Tenant signing certs (managed in CertySign portal)
1310
+ └── Tenant signing certs (HSM-backed, used for hash signing)
376
1311
  ```
377
1312
 
378
- CA bundle: `https://pki.certysign.com/certs/chain.pem`
379
- CRL: `https://pki.certysign.com/crl/intermediate.crl`
380
- OCSP: `https://pki.certysign.com/ocsp`
1313
+ CA bundle: `https://pki.certysign.io/certs/chain.pem`
1314
+ CRL: `https://pki.certysign.io/crl/intermediate.crl`
1315
+ OCSP: `https://pki.certysign.io/ocsp`
381
1316
 
382
1317
  ---
383
1318
 
384
1319
  ## Security Best Practices
385
1320
 
1321
+ - **Documents never leave your system** — only cryptographic hashes are sent to CertySign
386
1322
  - **Rotate keys** every 90 days: use the CertySign portal → API Keys → Rotate
387
1323
  - **IP allowlist** every production key to your deployment's egress IPs
388
1324
  - **Use `staging` environment** for development and CI pipelines (separate key pair)
389
1325
  - **Store the private key** from `certificates.issue()` response in a HSM or secrets manager — CertySign does not retain it
390
1326
  - **Cache CRL / OCSP responses** up to `nextUpdate` to reduce latency in HIE systems
1327
+ - **Verify hashes** after embedding — re-hash the original document to confirm integrity
1328
+ - **Secure OTP delivery** — OTPs are single-use, expire in 10 minutes, and lock after 5 failed attempts
1329
+ - **Signing tokens** — 30-minute expiry, cryptographically random, single-use
391
1330
 
392
1331
  ---
393
1332
 
394
1333
  ## Support
395
1334
 
396
- - Docs: https://docs.certysign.com/sdk
1335
+ - Docs: https://docs.certysign.io/sdk
397
1336
  - Issues: https://github.com/certysign/sdk-node/issues
398
- - Email: sdk@certysign.com
1337
+ - Email: sdk@certysign.io