@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
package/README.md
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
# @certysign/sdk
|
|
2
2
|
|
|
3
|
-
Official Node.js SDK for **CertySign Trust Services** — digital
|
|
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
|
-
|
|
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
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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.
|
|
107
|
-
| `staging` | `https://api-staging.certysign.
|
|
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` —
|
|
245
|
+
### `client.sign` — Hash-Based Signing
|
|
115
246
|
|
|
116
|
-
|
|
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.
|
|
120
|
-
document:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
reason:
|
|
125
|
-
location:
|
|
126
|
-
metadata:
|
|
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.
|
|
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.
|
|
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
|
-
#### `
|
|
306
|
+
#### `batchSignHashes(options)` — Sign multiple pre-computed hashes
|
|
134
307
|
|
|
135
308
|
```js
|
|
136
|
-
const result = await client.sign.
|
|
309
|
+
const result = await client.sign.batchSignHashes({
|
|
137
310
|
documents: [
|
|
138
|
-
{
|
|
139
|
-
{
|
|
311
|
+
{ documentHash: 'a3f2...', hashAlgorithm: 'sha256', fileName: 'doc1.pdf' },
|
|
312
|
+
{ documentHash: 'b7e1...', hashAlgorithm: 'sha256', fileName: 'doc2.pdf' }
|
|
140
313
|
],
|
|
141
|
-
signerName:
|
|
142
|
-
|
|
143
|
-
reason: 'Batch provider reimbursement'
|
|
314
|
+
signerName: 'Batch System',
|
|
315
|
+
reason: 'Monthly invoices'
|
|
144
316
|
});
|
|
145
|
-
//
|
|
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
|
-
|
|
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
|
|
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.
|
|
192
|
-
- **OCSP (AIA)** → `https://pki.certysign.
|
|
193
|
-
- **CA Issuers (AIA)** → `https://pki.certysign.
|
|
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
|
|
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.
|
|
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 `
|
|
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
|
-
##
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
|
364
|
-
|
|
365
|
-
|
|
|
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 (
|
|
1310
|
+
└── Tenant signing certs (HSM-backed, used for hash signing)
|
|
376
1311
|
```
|
|
377
1312
|
|
|
378
|
-
CA bundle: `https://pki.certysign.
|
|
379
|
-
CRL: `https://pki.certysign.
|
|
380
|
-
OCSP: `https://pki.certysign.
|
|
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.
|
|
1335
|
+
- Docs: https://docs.certysign.io/sdk
|
|
397
1336
|
- Issues: https://github.com/certysign/sdk-node/issues
|
|
398
|
-
- Email: sdk@certysign.
|
|
1337
|
+
- Email: sdk@certysign.io
|