@certysign/sdk 1.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 +398 -0
- package/examples/certificate-flow.js +198 -0
- package/examples/dha-integration.js +208 -0
- package/examples/nhif-batch-sign.js +216 -0
- package/package.json +45 -0
- package/src/index.js +161 -0
- package/src/lib/CertificateResource.js +146 -0
- package/src/lib/EnvelopeResource.js +249 -0
- package/src/lib/HttpClient.js +198 -0
- package/src/lib/PkiResource.js +170 -0
- package/src/lib/SigningResource.js +192 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview PkiResource — PKI infrastructure endpoints
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - crl() GET /sdk/v1/pki/crl — X.509 v2 CRL (RFC 5280)
|
|
6
|
+
* - ocsp() GET /sdk/v1/pki/ocsp/:sn — OCSP response (RFC 6960)
|
|
7
|
+
* - chain() GET /sdk/v1/pki/chain — CA certificate chain (PEM bundle)
|
|
8
|
+
* - info() GET /sdk/v1/pki/info — CA hierarchy metadata
|
|
9
|
+
*
|
|
10
|
+
* Typical use cases:
|
|
11
|
+
* - Cache the CRL locally in HIE systems for offline revocation checking
|
|
12
|
+
* - Perform in-process OCSP stapling
|
|
13
|
+
* - Download CA chain to install in a local trust store
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
class PkiResource {
|
|
19
|
+
/** @param {import('../lib/HttpClient').HttpClient} http */
|
|
20
|
+
constructor(http) {
|
|
21
|
+
this._http = http;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ── CRL ────────────────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Download the current Certificate Revocation List from CertySign Intermediate CA.
|
|
28
|
+
*
|
|
29
|
+
* The CRL is an RFC 5280 X.509 v2 CRL, signed by the Intermediate CA.
|
|
30
|
+
* - Valid for 7 days
|
|
31
|
+
* - Issued every 24 hours
|
|
32
|
+
* - Includes reasonCode per revoked certificate
|
|
33
|
+
*
|
|
34
|
+
* @param {'der'|'pem'|'json'} [format] - Response format (default: 'pem')
|
|
35
|
+
* @returns {Promise<Buffer|string|Object>}
|
|
36
|
+
* - 'der' → Buffer containing DER-encoded CRL binary
|
|
37
|
+
* - 'pem' → string with PEM-encoded CRL
|
|
38
|
+
* - 'json' → structured JSON with revocation list
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Download DER CRL and write to disk for caddy/nginx OCSP stapling cache
|
|
42
|
+
* const derBuf = await client.pki.crl('der');
|
|
43
|
+
* fs.writeFileSync('/etc/pki/certysign-intermediate.crl', derBuf);
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Get JSON for application-level revocation checks
|
|
47
|
+
* const { data } = await client.pki.crl('json');
|
|
48
|
+
* const revokedSerials = new Set(data.crl.map(e => e.serialNumber));
|
|
49
|
+
* if (revokedSerials.has(myCertSn)) throw new Error('Certificate has been revoked');
|
|
50
|
+
*/
|
|
51
|
+
async crl(format = 'pem') {
|
|
52
|
+
if (format === 'der') {
|
|
53
|
+
const response = await this._http.get('/sdk/v1/pki/crl', {
|
|
54
|
+
accept: 'application/pkix-crl',
|
|
55
|
+
responseType: 'arraybuffer'
|
|
56
|
+
});
|
|
57
|
+
return Buffer.isBuffer(response) ? response : Buffer.from(response);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (format === 'pem') {
|
|
61
|
+
return this._http.get('/sdk/v1/pki/crl', {
|
|
62
|
+
accept: 'application/x-pem-file',
|
|
63
|
+
responseType: 'text'
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Default: JSON
|
|
68
|
+
return this._http.get('/sdk/v1/pki/crl');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── OCSP ───────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Query the OCSP responder for a specific certificate.
|
|
75
|
+
*
|
|
76
|
+
* Returns an RFC 6960 BasicOCSPResponse — signed by CertySign Intermediate CA
|
|
77
|
+
* using SHA-256 with RSA. The CertID uses SHA-1 hashes as required by RFC 6960.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} serialNumber - Certificate serial number (hex string)
|
|
80
|
+
* @param {'der'|'json'} [format] - Response format (default: 'json')
|
|
81
|
+
* @returns {Promise<Buffer|OcspJsonResult>}
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* // Check revocation status for a certificate
|
|
85
|
+
* const { data } = await client.pki.ocsp('A1B2C3D4E5F6...', 'json');
|
|
86
|
+
* console.log(data.status); // 'good', 'revoked', or 'unknown'
|
|
87
|
+
* console.log(data.thisUpdate); // ISO 8601
|
|
88
|
+
* console.log(data.nextUpdate); // ISO 8601 — cache until this time
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Get DER response for OCSP stapling
|
|
92
|
+
* const derBuf = await client.pki.ocsp('A1B2C3D4E5F6...', 'der');
|
|
93
|
+
*/
|
|
94
|
+
async ocsp(serialNumber, format = 'json') {
|
|
95
|
+
if (!serialNumber) throw new Error('pki.ocsp: serialNumber is required');
|
|
96
|
+
|
|
97
|
+
if (format === 'der') {
|
|
98
|
+
const response = await this._http.get(`/sdk/v1/pki/ocsp/${encodeURIComponent(serialNumber)}`, {
|
|
99
|
+
accept: 'application/ocsp-response',
|
|
100
|
+
responseType: 'arraybuffer'
|
|
101
|
+
});
|
|
102
|
+
return Buffer.isBuffer(response) ? response : Buffer.from(response);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return this._http.get(`/sdk/v1/pki/ocsp/${encodeURIComponent(serialNumber)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── CA Chain ───────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Download the CertySign CA certificate chain as a PEM bundle.
|
|
112
|
+
*
|
|
113
|
+
* The bundle contains (in order):
|
|
114
|
+
* 1. CertySign Intermediate CA certificate
|
|
115
|
+
* 2. CertySign Root CA certificate
|
|
116
|
+
*
|
|
117
|
+
* Install this bundle in your trust store to verify all certificates
|
|
118
|
+
* issued by CertySign.
|
|
119
|
+
*
|
|
120
|
+
* @returns {Promise<string>} PEM-encoded certificate chain
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const chain = await client.pki.chain();
|
|
124
|
+
* fs.writeFileSync('/etc/ssl/certs/certysign-chain.pem', chain);
|
|
125
|
+
*
|
|
126
|
+
* // Verify a PEM cert against the chain (using openssl):
|
|
127
|
+
* // openssl verify -CAfile certysign-chain.pem your-cert.pem
|
|
128
|
+
*/
|
|
129
|
+
chain() {
|
|
130
|
+
return this._http.get('/sdk/v1/pki/chain', {
|
|
131
|
+
accept: 'application/x-pem-file',
|
|
132
|
+
responseType: 'text'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── CA Info ────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get metadata about the CertySign CA hierarchy.
|
|
140
|
+
*
|
|
141
|
+
* Returns information about the Root CA and Intermediate CA including:
|
|
142
|
+
* - Subject DN
|
|
143
|
+
* - Validity period
|
|
144
|
+
* - Key algorithm and size
|
|
145
|
+
* - CRL and OCSP URLs
|
|
146
|
+
* - Serial number
|
|
147
|
+
*
|
|
148
|
+
* @returns {Promise<CaInfoResult>}
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* const { data } = await client.pki.info();
|
|
152
|
+
* console.log(data.intermediateCA.subject.commonName);
|
|
153
|
+
* console.log(data.intermediateCA.crlUrl);
|
|
154
|
+
* console.log(data.status.initialized);
|
|
155
|
+
*/
|
|
156
|
+
info() {
|
|
157
|
+
return this._http.get('/sdk/v1/pki/info');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* @typedef {Object} OcspJsonResult
|
|
163
|
+
* @property {boolean} success
|
|
164
|
+
* @property {{ status: 'good'|'revoked'|'unknown', serialNumber: string,
|
|
165
|
+
* producedAt: string, thisUpdate: string, nextUpdate: string,
|
|
166
|
+
* responderId: string, revocationDate: string|null,
|
|
167
|
+
* revocationReason: string|null, der: string|null }} data
|
|
168
|
+
*/
|
|
169
|
+
|
|
170
|
+
module.exports = { PkiResource };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SigningResource — document signing operations
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - quickSign() POST /sdk/v1/sign
|
|
6
|
+
* - batchSign() POST /sdk/v1/sign/batch
|
|
7
|
+
* - verifyById() GET /sdk/v1/verify/:envelopeId
|
|
8
|
+
* - verifyDocument() POST /sdk/v1/verify
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const FormData = require('form-data');
|
|
16
|
+
|
|
17
|
+
class SigningResource {
|
|
18
|
+
/** @param {import('../lib/HttpClient').HttpClient} http */
|
|
19
|
+
constructor(http) {
|
|
20
|
+
this._http = http;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── Quick Sign ─────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sign a single document in one call.
|
|
27
|
+
*
|
|
28
|
+
* Uploads the document, creates an envelope, sends it, and signs it —
|
|
29
|
+
* all in a single API round-trip. Ideal for automation pipelines.
|
|
30
|
+
*
|
|
31
|
+
* @param {QuickSignOptions} options
|
|
32
|
+
* @returns {Promise<QuickSignResult>}
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* const result = await client.sign.quickSign({
|
|
36
|
+
* document: fs.readFileSync('./claim-form.pdf'),
|
|
37
|
+
* filename: 'claim-form.pdf',
|
|
38
|
+
* signerName: 'Dr. Amina Okonkwo',
|
|
39
|
+
* signerEmail: 'amina.okonkwo@dha.go.ke',
|
|
40
|
+
* reason: 'Health records approval — DHA Kenya',
|
|
41
|
+
* location: 'Nairobi, Kenya',
|
|
42
|
+
* metadata: { claimId: 'CLM-2026-003421', department: 'PHO' }
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* console.log(result.data.envelopeId); // Track the signed envelope
|
|
46
|
+
* console.log(result.data.certificate.serialNumber); // Per-doc X.509 cert
|
|
47
|
+
*/
|
|
48
|
+
async quickSign(options) {
|
|
49
|
+
const {
|
|
50
|
+
document,
|
|
51
|
+
filename = 'document.pdf',
|
|
52
|
+
signerName,
|
|
53
|
+
signerEmail,
|
|
54
|
+
reason = 'Digital signature',
|
|
55
|
+
location = 'Nairobi, Kenya',
|
|
56
|
+
metadata = {}
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!document) throw new Error('quickSign: document (Buffer|ReadStream) is required');
|
|
60
|
+
if (!signerName) throw new Error('quickSign: signerName is required');
|
|
61
|
+
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
_appendDocument(form, document, filename);
|
|
64
|
+
form.append('signerName', signerName);
|
|
65
|
+
if (signerEmail) form.append('signerEmail', signerEmail);
|
|
66
|
+
form.append('reason', reason);
|
|
67
|
+
form.append('location', location);
|
|
68
|
+
if (Object.keys(metadata).length) {
|
|
69
|
+
form.append('metadata', JSON.stringify(metadata));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this._http.post('/sdk/v1/sign', { formData: form });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── Batch Sign ─────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sign multiple documents in a single API call.
|
|
79
|
+
*
|
|
80
|
+
* Returns one result per document. Each document gets its own
|
|
81
|
+
* envelope and per-document X.509 certificate.
|
|
82
|
+
*
|
|
83
|
+
* @param {BatchSignOptions} options
|
|
84
|
+
* @returns {Promise<BatchSignResult>}
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* const result = await client.sign.batchSign({
|
|
88
|
+
* documents: [
|
|
89
|
+
* { data: fs.readFileSync('./invoice-001.pdf'), filename: 'invoice-001.pdf' },
|
|
90
|
+
* { data: fs.readFileSync('./invoice-002.pdf'), filename: 'invoice-002.pdf' }
|
|
91
|
+
* ],
|
|
92
|
+
* signerName: 'NHIF Finance System',
|
|
93
|
+
* signerEmail: 'invoicing@nhif.or.ke',
|
|
94
|
+
* reason: 'Batch invoice processing'
|
|
95
|
+
* });
|
|
96
|
+
*/
|
|
97
|
+
async batchSign(options) {
|
|
98
|
+
const {
|
|
99
|
+
documents,
|
|
100
|
+
signerName,
|
|
101
|
+
signerEmail,
|
|
102
|
+
reason = 'Batch digital signature',
|
|
103
|
+
location = 'Nairobi, Kenya',
|
|
104
|
+
metadata = {}
|
|
105
|
+
} = options;
|
|
106
|
+
|
|
107
|
+
if (!documents?.length) throw new Error('batchSign: documents array is required');
|
|
108
|
+
if (!signerName) throw new Error('batchSign: signerName is required');
|
|
109
|
+
|
|
110
|
+
const form = new FormData();
|
|
111
|
+
for (const doc of documents) {
|
|
112
|
+
_appendDocument(form, doc.data, doc.filename || 'document.pdf');
|
|
113
|
+
}
|
|
114
|
+
form.append('signerName', signerName);
|
|
115
|
+
if (signerEmail) form.append('signerEmail', signerEmail);
|
|
116
|
+
form.append('reason', reason);
|
|
117
|
+
form.append('location', location);
|
|
118
|
+
if (Object.keys(metadata).length) {
|
|
119
|
+
form.append('metadata', JSON.stringify(metadata));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this._http.post('/sdk/v1/sign/batch', { formData: form });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── Verify ─────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Verify the signature and certificate status of a previously signed envelope.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} envelopeId
|
|
131
|
+
* @returns {Promise<VerifyResult>}
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* const result = await client.sign.verifyById('env_abc123');
|
|
135
|
+
* if (result.data.valid) {
|
|
136
|
+
* console.log('Signature verified:', result.data.signerName);
|
|
137
|
+
* }
|
|
138
|
+
*/
|
|
139
|
+
verifyById(envelopeId) {
|
|
140
|
+
if (!envelopeId) throw new Error('verifyById: envelopeId is required');
|
|
141
|
+
return this._http.get(`/sdk/v1/verify/${encodeURIComponent(envelopeId)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Verify a document by uploading it (checks embedded signature).
|
|
146
|
+
*
|
|
147
|
+
* @param {Buffer|ReadStream} document
|
|
148
|
+
* @param {string} [filename]
|
|
149
|
+
* @returns {Promise<VerifyResult>}
|
|
150
|
+
*/
|
|
151
|
+
verifyDocument(document, filename = 'document.pdf') {
|
|
152
|
+
const form = new FormData();
|
|
153
|
+
_appendDocument(form, document, filename);
|
|
154
|
+
return this._http.post('/sdk/v1/verify', { formData: form });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
function _appendDocument(form, document, filename) {
|
|
161
|
+
if (Buffer.isBuffer(document)) {
|
|
162
|
+
form.append('document', document, { filename, contentType: 'application/pdf' });
|
|
163
|
+
} else if (document && typeof document.pipe === 'function') {
|
|
164
|
+
// ReadStream
|
|
165
|
+
form.append('document', document, { filename, contentType: 'application/pdf' });
|
|
166
|
+
} else {
|
|
167
|
+
throw new Error(`Document must be a Buffer or ReadStream, got ${typeof document}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @typedef {Object} QuickSignOptions
|
|
173
|
+
* @property {Buffer|import('fs').ReadStream} document - PDF file contents
|
|
174
|
+
* @property {string} [filename] - Filename for storage (default: 'document.pdf')
|
|
175
|
+
* @property {string} signerName - Full name of the signer
|
|
176
|
+
* @property {string} [signerEmail] - Email address of the signer
|
|
177
|
+
* @property {string} [reason] - Reason for signing
|
|
178
|
+
* @property {string} [location] - Physical signing location
|
|
179
|
+
* @property {Object} [metadata] - Custom key/value metadata stored with the envelope
|
|
180
|
+
*/
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* @typedef {Object} BatchSignOptions
|
|
184
|
+
* @property {{ data: Buffer|import('fs').ReadStream, filename?: string }[]} documents
|
|
185
|
+
* @property {string} signerName
|
|
186
|
+
* @property {string} [signerEmail]
|
|
187
|
+
* @property {string} [reason]
|
|
188
|
+
* @property {string} [location]
|
|
189
|
+
* @property {Object} [metadata]
|
|
190
|
+
*/
|
|
191
|
+
|
|
192
|
+
module.exports = { SigningResource };
|