@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/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@certysign/sdk",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Official Node.js SDK for CertySign — digital signing, X.509 certificates, and PKI services",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Official Node.js SDK for CertySign — hash-based digital signing, X.509 certificates, and PKI services. Documents never leave your system.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"src/",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
7
11
|
"scripts": {
|
|
8
12
|
"test": "jest --coverage",
|
|
9
13
|
"lint": "eslint src/**/*.js",
|
|
@@ -16,17 +20,21 @@
|
|
|
16
20
|
"x509",
|
|
17
21
|
"pdf-signing",
|
|
18
22
|
"document-signing",
|
|
23
|
+
"hash-based-signing",
|
|
19
24
|
"kenya",
|
|
20
|
-
"pades"
|
|
25
|
+
"pades",
|
|
26
|
+
"xmldsig",
|
|
27
|
+
"otp"
|
|
21
28
|
],
|
|
22
|
-
"author": "CertySign Limited <sdk@certysign.
|
|
29
|
+
"author": "CertySign Limited <sdk@certysign.io>",
|
|
23
30
|
"license": "MIT",
|
|
24
31
|
"engines": {
|
|
25
32
|
"node": ">=18.0.0"
|
|
26
33
|
},
|
|
27
34
|
"dependencies": {
|
|
28
35
|
"axios": "^1.6.0",
|
|
29
|
-
"form-data": "^4.0.0"
|
|
36
|
+
"form-data": "^4.0.0",
|
|
37
|
+
"pdf-lib": "^1.17.1"
|
|
30
38
|
},
|
|
31
39
|
"devDependencies": {
|
|
32
40
|
"jest": "^29.7.0"
|
|
@@ -35,7 +43,7 @@
|
|
|
35
43
|
"type": "git",
|
|
36
44
|
"url": "git+https://github.com/certysign/sdk-node.git"
|
|
37
45
|
},
|
|
38
|
-
"homepage": "https://docs.certysign.
|
|
46
|
+
"homepage": "https://docs.certysign.io/sdk",
|
|
39
47
|
"bugs": {
|
|
40
48
|
"url": "https://github.com/certysign/sdk-node/issues"
|
|
41
49
|
},
|
package/src/index.js
CHANGED
|
@@ -18,39 +18,59 @@
|
|
|
18
18
|
'use strict';
|
|
19
19
|
|
|
20
20
|
const { HttpClient, CertySignError } = require('./lib/HttpClient');
|
|
21
|
-
const { SigningResource }
|
|
22
|
-
const {
|
|
23
|
-
const {
|
|
24
|
-
const {
|
|
21
|
+
const { SigningResource } = require('./lib/SigningResource');
|
|
22
|
+
const { HashSigningResource } = require('./lib/HashSigningResource');
|
|
23
|
+
const { CertificateResource } = require('./lib/CertificateResource');
|
|
24
|
+
const { PkiResource } = require('./lib/PkiResource');
|
|
25
|
+
const { EnvelopeResource } = require('./lib/EnvelopeResource');
|
|
26
|
+
const { SigningSessionResource } = require('./lib/SigningSessionResource');
|
|
27
|
+
const { DashboardResource } = require('./lib/DashboardResource');
|
|
28
|
+
const { DocumentHasher } = require('./lib/DocumentHasher');
|
|
29
|
+
const { SignatureEmbedder } = require('./lib/SignatureEmbedder');
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* CertySign API client.
|
|
28
33
|
*
|
|
29
34
|
* Resources are accessed as properties:
|
|
30
|
-
* - client.sign — document signing (
|
|
31
|
-
* - client.
|
|
35
|
+
* - client.sign — hash-based document signing (documents stay local)
|
|
36
|
+
* - client.sessions — multi-recipient signing sessions with OTP verification
|
|
37
|
+
* - client.dashboard — SDK usage analytics (stats, recipients, documents)
|
|
38
|
+
* - client.certificates — X.509 certificate lifecycle (issue, verify, status, getActive)
|
|
32
39
|
* - client.pki — PKI infrastructure (CRL, OCSP, CA chain, info)
|
|
33
40
|
* - client.envelopes — envelope management (create, upload, send, sign, audit)
|
|
41
|
+
* - client.hasher — local document hashing utility
|
|
42
|
+
* - client.embedder — local signature embedding (PDF, XML, JSON)
|
|
43
|
+
* - client.legacySign — legacy file-upload signing (deprecated)
|
|
34
44
|
*
|
|
35
|
-
* @example
|
|
45
|
+
* @example Hash-based signing (documents never leave your system)
|
|
36
46
|
* const { CertySignClient } = require('@certysign/sdk');
|
|
37
47
|
*
|
|
38
48
|
* const client = new CertySignClient({
|
|
39
49
|
* publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
40
50
|
* secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
41
|
-
* environment: 'production'
|
|
51
|
+
* environment: 'production'
|
|
42
52
|
* });
|
|
43
53
|
*
|
|
44
|
-
* //
|
|
45
|
-
* const result = await client.sign.
|
|
46
|
-
* document:
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* reason: 'Health records approval'
|
|
54
|
+
* // 1. Hash locally → sign remotely → embed locally
|
|
55
|
+
* const result = await client.sign.hashAndSign({
|
|
56
|
+
* document: fs.readFileSync('./contract.pdf'),
|
|
57
|
+
* fileName: 'contract.pdf',
|
|
58
|
+
* reason: 'Contract approval'
|
|
50
59
|
* });
|
|
51
60
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
61
|
+
* // 2. Embed signature into the PDF on your system
|
|
62
|
+
* const signedPdf = await client.embedder.embedInPdf(
|
|
63
|
+
* fs.readFileSync('./contract.pdf'),
|
|
64
|
+
* {
|
|
65
|
+
* signature: result.data.signature,
|
|
66
|
+
* certificate: result.data.certificate,
|
|
67
|
+
* chain: result.data.chain,
|
|
68
|
+
* signerName: 'Dr. Amina Okonkwo',
|
|
69
|
+
* reason: 'Contract approval',
|
|
70
|
+
* certSerialNumber: result.data.certSerialNumber
|
|
71
|
+
* }
|
|
72
|
+
* );
|
|
73
|
+
* fs.writeFileSync('./contract-signed.pdf', signedPdf);
|
|
54
74
|
*/
|
|
55
75
|
class CertySignClient {
|
|
56
76
|
/**
|
|
@@ -90,10 +110,15 @@ class CertySignClient {
|
|
|
90
110
|
});
|
|
91
111
|
|
|
92
112
|
// ── Resource objects ──
|
|
93
|
-
this.sign = new
|
|
113
|
+
this.sign = new HashSigningResource(this._http); // Hash-based signing (documents stay local)
|
|
114
|
+
this.legacySign = new SigningResource(this._http); // Legacy file-upload signing
|
|
94
115
|
this.certificates = new CertificateResource(this._http);
|
|
95
116
|
this.pki = new PkiResource(this._http);
|
|
96
117
|
this.envelopes = new EnvelopeResource(this._http);
|
|
118
|
+
this.sessions = new SigningSessionResource(this._http); // Multi-recipient signing sessions
|
|
119
|
+
this.dashboard = new DashboardResource(this._http); // SDK usage analytics
|
|
120
|
+
this.hasher = new DocumentHasher(); // Local document hashing
|
|
121
|
+
this.embedder = new SignatureEmbedder(); // Local signature embedding
|
|
97
122
|
|
|
98
123
|
this.publicKey = publicKey;
|
|
99
124
|
this.environment = environment;
|
|
@@ -106,8 +131,8 @@ class CertySignClient {
|
|
|
106
131
|
*/
|
|
107
132
|
static get BASE_URLS() {
|
|
108
133
|
return {
|
|
109
|
-
production: 'https://
|
|
110
|
-
staging: 'https://
|
|
134
|
+
production: 'https://core.certysign.io',
|
|
135
|
+
staging: 'https://service.certysign.io',
|
|
111
136
|
development: 'http://localhost:8000',
|
|
112
137
|
test: 'http://localhost:8000'
|
|
113
138
|
};
|
|
@@ -157,5 +182,15 @@ class CertySignClient {
|
|
|
157
182
|
|
|
158
183
|
module.exports = {
|
|
159
184
|
CertySignClient,
|
|
160
|
-
CertySignError
|
|
185
|
+
CertySignError,
|
|
186
|
+
DocumentHasher,
|
|
187
|
+
SignatureEmbedder,
|
|
188
|
+
HashSigningResource,
|
|
189
|
+
SigningSessionResource,
|
|
190
|
+
DashboardResource,
|
|
191
|
+
// Legacy exports
|
|
192
|
+
SigningResource,
|
|
193
|
+
CertificateResource,
|
|
194
|
+
PkiResource,
|
|
195
|
+
EnvelopeResource
|
|
161
196
|
};
|
|
@@ -26,9 +26,9 @@ class CertificateResource {
|
|
|
26
26
|
*
|
|
27
27
|
* The certificate is bound to the subscriber's organisation and the specific
|
|
28
28
|
* document. It includes:
|
|
29
|
-
* - CRL Distribution Point → https://pki.certysign.
|
|
30
|
-
* - OCSP AIA → https://pki.certysign.
|
|
31
|
-
* - CA Issuers AIA → https://pki.certysign.
|
|
29
|
+
* - CRL Distribution Point → https://pki.certysign.io/crl/intermediate.crl
|
|
30
|
+
* - OCSP AIA → https://pki.certysign.io/ocsp
|
|
31
|
+
* - CA Issuers AIA → https://pki.certysign.io/certs/intermediate.crt
|
|
32
32
|
*
|
|
33
33
|
* The private key is returned ONCE in the response (PEM). CertySign does not
|
|
34
34
|
* retain the private key after issuance.
|
|
@@ -122,6 +122,27 @@ class CertificateResource {
|
|
|
122
122
|
if (!serialNumber) throw new Error('certificates.status: serialNumber is required');
|
|
123
123
|
return this._http.get(`/sdk/v1/certificates/${encodeURIComponent(serialNumber)}/status`);
|
|
124
124
|
}
|
|
125
|
+
|
|
126
|
+
// ── Active Certificate ─────────────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get the tenant's active signing certificate.
|
|
130
|
+
* The certificate is resolved based on the API keys used for authentication.
|
|
131
|
+
*
|
|
132
|
+
* Returns the PEM certificate, chain, serial number, fingerprint,
|
|
133
|
+
* and validity information.
|
|
134
|
+
*
|
|
135
|
+
* @returns {Promise<Object>}
|
|
136
|
+
*
|
|
137
|
+
* @example
|
|
138
|
+
* const { data } = await client.certificates.getActive();
|
|
139
|
+
* console.log(data.certSerialNumber);
|
|
140
|
+
* console.log(data.certificate); // PEM
|
|
141
|
+
* console.log(data.validUntil);
|
|
142
|
+
*/
|
|
143
|
+
getActive() {
|
|
144
|
+
return this._http.get('/sdk/v1/certificates/active');
|
|
145
|
+
}
|
|
125
146
|
}
|
|
126
147
|
|
|
127
148
|
/**
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DashboardResource — SDK usage analytics
|
|
3
|
+
*
|
|
4
|
+
* Access dashboard statistics for your SDK-based signing operations:
|
|
5
|
+
* - Documents signed (total, today, this month)
|
|
6
|
+
* - Unique recipients across all signing sessions
|
|
7
|
+
* - Success / failure rates
|
|
8
|
+
* - Daily signing trends
|
|
9
|
+
* - Signed document listing with recipient details
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
class DashboardResource {
|
|
15
|
+
/** @param {import('./HttpClient').HttpClient} http */
|
|
16
|
+
constructor(http) {
|
|
17
|
+
this._http = http;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get aggregate statistics for SDK signing activity.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} [options]
|
|
24
|
+
* @param {string} [options.from] - ISO date — start of range (default: 30 days ago)
|
|
25
|
+
* @param {string} [options.to] - ISO date — end of range (default: now)
|
|
26
|
+
* @returns {Promise<Object>}
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* const { data } = await client.dashboard.getStats();
|
|
30
|
+
* console.log('Documents signed today:', data.documentsSignedToday);
|
|
31
|
+
* console.log('Unique recipients:', data.uniqueRecipientsCount);
|
|
32
|
+
* console.log('Success rate:', data.successRate + '%');
|
|
33
|
+
*/
|
|
34
|
+
getStats(options = {}) {
|
|
35
|
+
const params = {};
|
|
36
|
+
if (options.from) params.from = options.from;
|
|
37
|
+
if (options.to) params.to = options.to;
|
|
38
|
+
|
|
39
|
+
return this._http.get('/sdk/v1/dashboard/stats', { params });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* List unique recipients across all signing sessions.
|
|
44
|
+
*
|
|
45
|
+
* @param {Object} [options]
|
|
46
|
+
* @param {number} [options.page] - Page number (default: 1)
|
|
47
|
+
* @param {number} [options.limit] - Results per page (default: 50)
|
|
48
|
+
* @param {string} [options.search] - Search by email or name
|
|
49
|
+
* @returns {Promise<Object>}
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* const { data } = await client.dashboard.getRecipients({ search: 'ceo' });
|
|
53
|
+
* for (const r of data.recipients) {
|
|
54
|
+
* console.log(`${r.email}: signed ${r.signedSessions} sessions`);
|
|
55
|
+
* }
|
|
56
|
+
*/
|
|
57
|
+
getRecipients(options = {}) {
|
|
58
|
+
const params = {};
|
|
59
|
+
if (options.page) params.page = options.page;
|
|
60
|
+
if (options.limit) params.limit = options.limit;
|
|
61
|
+
if (options.search) params.search = options.search;
|
|
62
|
+
|
|
63
|
+
return this._http.get('/sdk/v1/dashboard/recipients', { params });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* List documents across all signing sessions with signature details.
|
|
68
|
+
*
|
|
69
|
+
* @param {Object} [options]
|
|
70
|
+
* @param {number} [options.page] - Page number (default: 1)
|
|
71
|
+
* @param {number} [options.limit] - Results per page (default: 20)
|
|
72
|
+
* @param {string} [options.status] - Filter by session status: 'active' | 'completed' | 'expired'
|
|
73
|
+
* @returns {Promise<Object>}
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* const { data } = await client.dashboard.getDocuments({ status: 'completed' });
|
|
77
|
+
* for (const doc of data.documents) {
|
|
78
|
+
* console.log(`${doc.fileName}: ${doc.signatureCount} signatures`);
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
getDocuments(options = {}) {
|
|
82
|
+
const params = {};
|
|
83
|
+
if (options.page) params.page = options.page;
|
|
84
|
+
if (options.limit) params.limit = options.limit;
|
|
85
|
+
if (options.status) params.status = options.status;
|
|
86
|
+
|
|
87
|
+
return this._http.get('/sdk/v1/dashboard/documents', { params });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { DashboardResource };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview DocumentHasher — local document hashing
|
|
3
|
+
*
|
|
4
|
+
* Hashes documents on the subscriber's system so the raw document
|
|
5
|
+
* NEVER leaves their infrastructure. Only the hash is sent to CertySign.
|
|
6
|
+
*
|
|
7
|
+
* Supports: SHA-256, SHA-384, SHA-512
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const SUPPORTED_ALGORITHMS = ['sha256', 'sha384', 'sha512'];
|
|
17
|
+
|
|
18
|
+
class DocumentHasher {
|
|
19
|
+
/**
|
|
20
|
+
* @param {Object} [options]
|
|
21
|
+
* @param {string} [options.algorithm='sha256'] - Hash algorithm
|
|
22
|
+
*/
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.algorithm = (options.algorithm || 'sha256').toLowerCase();
|
|
25
|
+
if (!SUPPORTED_ALGORITHMS.includes(this.algorithm)) {
|
|
26
|
+
throw new Error(`Unsupported algorithm: ${this.algorithm}. Use: ${SUPPORTED_ALGORITHMS.join(', ')}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hash a document from a Buffer or string.
|
|
32
|
+
*
|
|
33
|
+
* @param {Buffer|string} data - Document contents
|
|
34
|
+
* @param {string} [algorithm] - Override default algorithm
|
|
35
|
+
* @returns {{ hash: string, algorithm: string, size: number }}
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const hasher = new DocumentHasher();
|
|
39
|
+
* const result = hasher.hash(fs.readFileSync('./contract.pdf'));
|
|
40
|
+
* console.log(result.hash); // "a1b2c3..."
|
|
41
|
+
*/
|
|
42
|
+
hash(data, algorithm) {
|
|
43
|
+
const algo = (algorithm || this.algorithm).toLowerCase();
|
|
44
|
+
if (!SUPPORTED_ALGORITHMS.includes(algo)) {
|
|
45
|
+
throw new Error(`Unsupported algorithm: ${algo}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
49
|
+
const hash = crypto.createHash(algo).update(buf).digest('hex');
|
|
50
|
+
|
|
51
|
+
return { hash, algorithm: algo, size: buf.length };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Hash a document from a file path.
|
|
56
|
+
*
|
|
57
|
+
* Uses streaming to handle large files without loading the entire
|
|
58
|
+
* file into memory.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} filePath - Absolute or relative file path
|
|
61
|
+
* @param {string} [algorithm] - Override default algorithm
|
|
62
|
+
* @returns {Promise<{ hash: string, algorithm: string, size: number, fileName: string }>}
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* const hasher = new DocumentHasher();
|
|
66
|
+
* const result = await hasher.hashFile('./contract.pdf');
|
|
67
|
+
* console.log(result.hash);
|
|
68
|
+
* console.log(result.fileName); // "contract.pdf"
|
|
69
|
+
*/
|
|
70
|
+
hashFile(filePath, algorithm) {
|
|
71
|
+
const algo = (algorithm || this.algorithm).toLowerCase();
|
|
72
|
+
if (!SUPPORTED_ALGORITHMS.includes(algo)) {
|
|
73
|
+
return Promise.reject(new Error(`Unsupported algorithm: ${algo}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const hash = crypto.createHash(algo);
|
|
78
|
+
let size = 0;
|
|
79
|
+
|
|
80
|
+
const stream = fs.createReadStream(filePath);
|
|
81
|
+
stream.on('data', (chunk) => {
|
|
82
|
+
hash.update(chunk);
|
|
83
|
+
size += chunk.length;
|
|
84
|
+
});
|
|
85
|
+
stream.on('end', () => {
|
|
86
|
+
resolve({
|
|
87
|
+
hash: hash.digest('hex'),
|
|
88
|
+
algorithm: algo,
|
|
89
|
+
size,
|
|
90
|
+
fileName: path.basename(filePath)
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
stream.on('error', reject);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Hash multiple documents (Buffers).
|
|
99
|
+
*
|
|
100
|
+
* @param {{ data: Buffer, fileName: string }[]} documents
|
|
101
|
+
* @param {string} [algorithm]
|
|
102
|
+
* @returns {{ hash: string, algorithm: string, size: number, fileName: string }[]}
|
|
103
|
+
*/
|
|
104
|
+
hashMany(documents, algorithm) {
|
|
105
|
+
return documents.map(doc => {
|
|
106
|
+
const result = this.hash(doc.data, algorithm);
|
|
107
|
+
return { ...result, fileName: doc.fileName };
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Hash multiple files by path.
|
|
113
|
+
*
|
|
114
|
+
* @param {string[]} filePaths
|
|
115
|
+
* @param {string} [algorithm]
|
|
116
|
+
* @returns {Promise<{ hash: string, algorithm: string, size: number, fileName: string }[]>}
|
|
117
|
+
*/
|
|
118
|
+
hashFiles(filePaths, algorithm) {
|
|
119
|
+
return Promise.all(filePaths.map(fp => this.hashFile(fp, algorithm)));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { DocumentHasher };
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview HashSigningResource — hash-based document signing
|
|
3
|
+
*
|
|
4
|
+
* Documents NEVER leave the subscriber's system.
|
|
5
|
+
* Flow: hash locally → send hash to CertySign → receive signature → embed locally.
|
|
6
|
+
*
|
|
7
|
+
* Covers:
|
|
8
|
+
* - hashAndSign() Hash a document and sign in one call
|
|
9
|
+
* - signHash() Sign a pre-computed hash
|
|
10
|
+
* - batchHashAndSign() Hash and sign multiple documents
|
|
11
|
+
* - batchSignHashes() Sign multiple pre-computed hashes
|
|
12
|
+
* - verifyById() Verify an envelope's signatures
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const { DocumentHasher } = require('./DocumentHasher');
|
|
18
|
+
|
|
19
|
+
class HashSigningResource {
|
|
20
|
+
/**
|
|
21
|
+
* @param {import('./HttpClient').HttpClient} http
|
|
22
|
+
*/
|
|
23
|
+
constructor(http) {
|
|
24
|
+
this._http = http;
|
|
25
|
+
this._hasher = new DocumentHasher();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Hash + Sign (single document) ─────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Hash a document locally and send the hash to CertySign for signing.
|
|
32
|
+
* The document never leaves your system.
|
|
33
|
+
*
|
|
34
|
+
* @param {HashAndSignOptions} options
|
|
35
|
+
* @returns {Promise<HashSignResult>}
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const result = await client.sign.hashAndSign({
|
|
39
|
+
* document: fs.readFileSync('./contract.pdf'),
|
|
40
|
+
* fileName: 'contract.pdf',
|
|
41
|
+
* reason: 'Contract approval',
|
|
42
|
+
* location: 'Nairobi, Kenya'
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* console.log(result.data.signature); // Base64 CMS/PKCS#7
|
|
46
|
+
* console.log(result.data.certificate); // PEM certificate
|
|
47
|
+
* console.log(result.data.documentHash); // SHA-256 hex
|
|
48
|
+
*/
|
|
49
|
+
async hashAndSign(options) {
|
|
50
|
+
const {
|
|
51
|
+
document,
|
|
52
|
+
fileName = 'document.pdf',
|
|
53
|
+
reason = 'Digital signature',
|
|
54
|
+
location = 'Nairobi, Kenya',
|
|
55
|
+
hashAlgorithm = 'sha256',
|
|
56
|
+
metadata = {}
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!document) throw new Error('hashAndSign: document (Buffer) is required');
|
|
60
|
+
|
|
61
|
+
// Hash locally — document stays on subscriber system
|
|
62
|
+
const { hash } = this._hasher.hash(document, hashAlgorithm);
|
|
63
|
+
|
|
64
|
+
// Send only the hash to CertySign
|
|
65
|
+
return this._http.post('/sdk/v1/sign/hash', {
|
|
66
|
+
data: {
|
|
67
|
+
documentHash: hash,
|
|
68
|
+
hashAlgorithm,
|
|
69
|
+
fileName,
|
|
70
|
+
reason,
|
|
71
|
+
location,
|
|
72
|
+
metadata
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── Sign pre-computed hash ─────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Sign a pre-computed document hash.
|
|
81
|
+
* Use this when you've already computed the hash yourself.
|
|
82
|
+
*
|
|
83
|
+
* @param {SignHashOptions} options
|
|
84
|
+
* @returns {Promise<HashSignResult>}
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const hash = crypto.createHash('sha256').update(pdfBuffer).digest('hex');
|
|
88
|
+
* const result = await client.sign.signHash({
|
|
89
|
+
* documentHash: hash,
|
|
90
|
+
* hashAlgorithm: 'sha256',
|
|
91
|
+
* fileName: 'invoice.pdf',
|
|
92
|
+
* reason: 'Invoice approval'
|
|
93
|
+
* });
|
|
94
|
+
*/
|
|
95
|
+
async signHash(options) {
|
|
96
|
+
const {
|
|
97
|
+
documentHash,
|
|
98
|
+
hashAlgorithm = 'sha256',
|
|
99
|
+
fileName,
|
|
100
|
+
reason = 'Digital signature',
|
|
101
|
+
location = 'Nairobi, Kenya',
|
|
102
|
+
metadata = {}
|
|
103
|
+
} = options;
|
|
104
|
+
|
|
105
|
+
if (!documentHash) throw new Error('signHash: documentHash is required');
|
|
106
|
+
|
|
107
|
+
return this._http.post('/sdk/v1/sign/hash', {
|
|
108
|
+
data: { documentHash, hashAlgorithm, fileName, reason, location, metadata }
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Batch hash + sign ──────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Hash and sign multiple documents in one API call.
|
|
116
|
+
* All documents are hashed locally — none leave your system.
|
|
117
|
+
*
|
|
118
|
+
* @param {BatchHashAndSignOptions} options
|
|
119
|
+
* @returns {Promise<BatchHashSignResult>}
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* const result = await client.sign.batchHashAndSign({
|
|
123
|
+
* documents: [
|
|
124
|
+
* { data: fs.readFileSync('./doc1.pdf'), fileName: 'doc1.pdf' },
|
|
125
|
+
* { data: fs.readFileSync('./doc2.pdf'), fileName: 'doc2.pdf' }
|
|
126
|
+
* ],
|
|
127
|
+
* reason: 'Batch approval'
|
|
128
|
+
* });
|
|
129
|
+
*/
|
|
130
|
+
async batchHashAndSign(options) {
|
|
131
|
+
const {
|
|
132
|
+
documents,
|
|
133
|
+
reason = 'Batch digital signature',
|
|
134
|
+
hashAlgorithm = 'sha256',
|
|
135
|
+
metadata = {}
|
|
136
|
+
} = options;
|
|
137
|
+
|
|
138
|
+
if (!documents?.length) throw new Error('batchHashAndSign: documents array is required');
|
|
139
|
+
|
|
140
|
+
// Hash all documents locally
|
|
141
|
+
const hashed = documents.map(doc => {
|
|
142
|
+
const { hash } = this._hasher.hash(doc.data, hashAlgorithm);
|
|
143
|
+
return {
|
|
144
|
+
documentHash: hash,
|
|
145
|
+
hashAlgorithm,
|
|
146
|
+
fileName: doc.fileName || 'document.pdf'
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return this._http.post('/sdk/v1/sign/hash/batch', {
|
|
151
|
+
data: { documents: hashed, reason, metadata }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Sign multiple pre-computed hashes in one API call.
|
|
157
|
+
*
|
|
158
|
+
* @param {BatchSignHashesOptions} options
|
|
159
|
+
* @returns {Promise<BatchHashSignResult>}
|
|
160
|
+
*/
|
|
161
|
+
async batchSignHashes(options) {
|
|
162
|
+
const { documents, reason = 'Batch digital signature', metadata = {} } = options;
|
|
163
|
+
|
|
164
|
+
if (!documents?.length) throw new Error('batchSignHashes: documents array is required');
|
|
165
|
+
|
|
166
|
+
return this._http.post('/sdk/v1/sign/hash/batch', {
|
|
167
|
+
data: { documents, reason, metadata }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── Verify ─────────────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Verify the signature of a previously signed envelope.
|
|
175
|
+
*
|
|
176
|
+
* @param {string} envelopeId
|
|
177
|
+
* @returns {Promise<Object>}
|
|
178
|
+
*/
|
|
179
|
+
verifyById(envelopeId) {
|
|
180
|
+
if (!envelopeId) throw new Error('verifyById: envelopeId is required');
|
|
181
|
+
return this._http.get(`/sdk/v1/verify/${encodeURIComponent(envelopeId)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @typedef {Object} HashAndSignOptions
|
|
187
|
+
* @property {Buffer} document - Document contents (stays local)
|
|
188
|
+
* @property {string} [fileName] - File name
|
|
189
|
+
* @property {string} [reason] - Signing reason
|
|
190
|
+
* @property {string} [location] - Signing location
|
|
191
|
+
* @property {string} [hashAlgorithm] - sha256 | sha384 | sha512
|
|
192
|
+
* @property {Object} [metadata] - Custom key/value metadata
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* @typedef {Object} SignHashOptions
|
|
197
|
+
* @property {string} documentHash - Hex-encoded hash
|
|
198
|
+
* @property {string} [hashAlgorithm] - sha256 | sha384 | sha512
|
|
199
|
+
* @property {string} [fileName]
|
|
200
|
+
* @property {string} [reason]
|
|
201
|
+
* @property {string} [location]
|
|
202
|
+
* @property {Object} [metadata]
|
|
203
|
+
*/
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* @typedef {Object} BatchHashAndSignOptions
|
|
207
|
+
* @property {{ data: Buffer, fileName?: string }[]} documents
|
|
208
|
+
* @property {string} [reason]
|
|
209
|
+
* @property {string} [hashAlgorithm]
|
|
210
|
+
* @property {Object} [metadata]
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @typedef {Object} BatchSignHashesOptions
|
|
215
|
+
* @property {{ documentHash: string, hashAlgorithm?: string, fileName?: string }[]} documents
|
|
216
|
+
* @property {string} [reason]
|
|
217
|
+
* @property {Object} [metadata]
|
|
218
|
+
*/
|
|
219
|
+
|
|
220
|
+
module.exports = { HashSigningResource };
|
package/src/lib/HttpClient.js
CHANGED
|
@@ -15,7 +15,7 @@ const FormData = require('form-data');
|
|
|
15
15
|
const crypto = require('crypto');
|
|
16
16
|
|
|
17
17
|
/** Default base URL — override per environment */
|
|
18
|
-
const DEFAULT_BASE_URL = 'https://
|
|
18
|
+
const DEFAULT_BASE_URL = 'https://core.certysign.io';
|
|
19
19
|
|
|
20
20
|
/** Statuses that should trigger a retry */
|
|
21
21
|
const RETRYABLE_STATUS = new Set([429, 502, 503, 504]);
|