@certysign/sdk 1.0.0 → 2.1.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 +15 -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 +723 -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
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview SigningSessionResource — multi-recipient hash-based signing
|
|
3
|
+
*
|
|
4
|
+
* Manages signing sessions where:
|
|
5
|
+
* - Document hashes are registered (documents stay local)
|
|
6
|
+
* - Recipients are added with sequential or parallel signing order
|
|
7
|
+
* - OTP verification is required before each recipient can sign
|
|
8
|
+
* - HSM signs the hash for each verified recipient
|
|
9
|
+
*
|
|
10
|
+
* Covers:
|
|
11
|
+
* - create() Create a signing session
|
|
12
|
+
* - get() Get session status
|
|
13
|
+
* - list() List sessions
|
|
14
|
+
* - sendOtp() Send OTP to a recipient
|
|
15
|
+
* - verifyOtp() Verify recipient OTP
|
|
16
|
+
* - recipientSign() Recipient signs after OTP verification
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
class SigningSessionResource {
|
|
22
|
+
/** @param {import('./HttpClient').HttpClient} http */
|
|
23
|
+
constructor(http) {
|
|
24
|
+
this._http = http;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a signing session with document hashes and recipients.
|
|
29
|
+
*
|
|
30
|
+
* @param {CreateSessionOptions} options
|
|
31
|
+
* @returns {Promise<Object>}
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* const session = await client.sessions.create({
|
|
35
|
+
* name: 'Q1 Contract',
|
|
36
|
+
* documents: [
|
|
37
|
+
* { hash: 'abc123...', fileName: 'contract.pdf', hashAlgorithm: 'sha256' }
|
|
38
|
+
* ],
|
|
39
|
+
* recipients: [
|
|
40
|
+
* { email: 'ceo@example.com', name: 'Jane CEO', role: 'signer', order: 1 },
|
|
41
|
+
* { email: 'cfo@example.com', name: 'John CFO', role: 'signer', order: 2 }
|
|
42
|
+
* ],
|
|
43
|
+
* signingOrder: 'sequential'
|
|
44
|
+
* });
|
|
45
|
+
*
|
|
46
|
+
* console.log(session.data._id); // Session ID
|
|
47
|
+
* console.log(session.data.recipients[0].recipientId); // Use for OTP/sign
|
|
48
|
+
*/
|
|
49
|
+
create(options) {
|
|
50
|
+
const {
|
|
51
|
+
name,
|
|
52
|
+
description,
|
|
53
|
+
documents,
|
|
54
|
+
recipients,
|
|
55
|
+
signingOrder = 'parallel',
|
|
56
|
+
expiresAt
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!name) throw new Error('create: name is required');
|
|
60
|
+
if (!documents?.length) throw new Error('create: documents array is required');
|
|
61
|
+
if (!recipients?.length) throw new Error('create: recipients array is required');
|
|
62
|
+
|
|
63
|
+
return this._http.post('/sdk/v1/signing-sessions', {
|
|
64
|
+
data: { name, description, documents, recipients, signingOrder, expiresAt }
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Get a signing session by ID.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} sessionId
|
|
72
|
+
* @returns {Promise<Object>}
|
|
73
|
+
*/
|
|
74
|
+
get(sessionId) {
|
|
75
|
+
if (!sessionId) throw new Error('get: sessionId is required');
|
|
76
|
+
return this._http.get(`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List signing sessions with pagination.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} [options]
|
|
83
|
+
* @param {number} [options.page=1]
|
|
84
|
+
* @param {number} [options.limit=20]
|
|
85
|
+
* @param {string} [options.status]
|
|
86
|
+
* @returns {Promise<Object>}
|
|
87
|
+
*/
|
|
88
|
+
list(options = {}) {
|
|
89
|
+
return this._http.get('/sdk/v1/signing-sessions', { params: options });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Send an OTP verification code to a recipient's email.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} sessionId
|
|
96
|
+
* @param {string} recipientId
|
|
97
|
+
* @returns {Promise<Object>}
|
|
98
|
+
*
|
|
99
|
+
* @example
|
|
100
|
+
* await client.sessions.sendOtp(sessionId, recipientId);
|
|
101
|
+
* // Recipient receives a 6-digit code via email
|
|
102
|
+
*/
|
|
103
|
+
sendOtp(sessionId, recipientId) {
|
|
104
|
+
if (!sessionId) throw new Error('sendOtp: sessionId is required');
|
|
105
|
+
if (!recipientId) throw new Error('sendOtp: recipientId is required');
|
|
106
|
+
return this._http.post(
|
|
107
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/send-otp`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Verify a recipient's OTP code.
|
|
113
|
+
* On success, returns a short-lived signing token.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} sessionId
|
|
116
|
+
* @param {string} recipientId
|
|
117
|
+
* @param {string} code - 6-digit OTP from email
|
|
118
|
+
* @returns {Promise<Object>} - { signingToken, expiresAt }
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const result = await client.sessions.verifyOtp(sessionId, recipientId, '123456');
|
|
122
|
+
* const signingToken = result.data.signingToken;
|
|
123
|
+
*/
|
|
124
|
+
verifyOtp(sessionId, recipientId, code) {
|
|
125
|
+
if (!sessionId) throw new Error('verifyOtp: sessionId is required');
|
|
126
|
+
if (!recipientId) throw new Error('verifyOtp: recipientId is required');
|
|
127
|
+
if (!code) throw new Error('verifyOtp: code is required');
|
|
128
|
+
|
|
129
|
+
return this._http.post(
|
|
130
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/verify-otp`,
|
|
131
|
+
{ data: { code: String(code) } }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recipient signs all documents in the session.
|
|
137
|
+
* Requires the signing token obtained from verifyOtp().
|
|
138
|
+
*
|
|
139
|
+
* @param {string} sessionId
|
|
140
|
+
* @param {string} recipientId
|
|
141
|
+
* @param {string} signingToken - Token from verifyOtp()
|
|
142
|
+
* @returns {Promise<Object>} - { signedDocuments, certificate, chain }
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* const result = await client.sessions.recipientSign(
|
|
146
|
+
* sessionId, recipientId, signingToken
|
|
147
|
+
* );
|
|
148
|
+
*
|
|
149
|
+
* // Use SignatureEmbedder to embed signatures into local documents
|
|
150
|
+
* for (const doc of result.data.signedDocuments) {
|
|
151
|
+
* const signedPdf = await client.embedder.embedInPdf(pdfBuffer, {
|
|
152
|
+
* signature: doc.signature,
|
|
153
|
+
* certificate: result.data.certificate,
|
|
154
|
+
* chain: result.data.chain,
|
|
155
|
+
* reason: 'Contract signing',
|
|
156
|
+
* signerName: 'Jane CEO'
|
|
157
|
+
* });
|
|
158
|
+
* fs.writeFileSync(`./signed-${doc.fileName}`, signedPdf);
|
|
159
|
+
* }
|
|
160
|
+
*/
|
|
161
|
+
recipientSign(sessionId, recipientId, signingToken) {
|
|
162
|
+
if (!sessionId) throw new Error('recipientSign: sessionId is required');
|
|
163
|
+
if (!recipientId) throw new Error('recipientSign: recipientId is required');
|
|
164
|
+
if (!signingToken) throw new Error('recipientSign: signingToken is required');
|
|
165
|
+
|
|
166
|
+
return this._http.post(
|
|
167
|
+
`/sdk/v1/signing-sessions/${encodeURIComponent(sessionId)}/recipients/${encodeURIComponent(recipientId)}/sign`,
|
|
168
|
+
{ data: { signingToken } }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* @typedef {Object} CreateSessionOptions
|
|
175
|
+
* @property {string} name - Session name
|
|
176
|
+
* @property {string} [description]
|
|
177
|
+
* @property {{ hash: string, fileName: string, hashAlgorithm?: string, mimeType?: string }[]} documents
|
|
178
|
+
* @property {{ email: string, name: string, role?: string, order?: number }[]} recipients
|
|
179
|
+
* @property {'sequential'|'parallel'} [signingOrder='parallel']
|
|
180
|
+
* @property {string} [expiresAt] - ISO date
|
|
181
|
+
*/
|
|
182
|
+
|
|
183
|
+
module.exports = { SigningSessionResource };
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Full PKI Certificate Lifecycle Example
|
|
3
|
-
*
|
|
4
|
-
* Demonstrates the complete X.509 certificate workflow:
|
|
5
|
-
* 1. Issue a per-document certificate
|
|
6
|
-
* 2. Check current status (should be 'good')
|
|
7
|
-
* 3. Verify the certificate (chain, revocation, validity)
|
|
8
|
-
* 4. Download CA chain and save it locally
|
|
9
|
-
* 5. Perform a point-in-time verification
|
|
10
|
-
* 6. Fetch and cache the CRL locally
|
|
11
|
-
* 7. Check a serial number directly against the CRL JSON
|
|
12
|
-
* 8. Perform an OCSP query
|
|
13
|
-
*
|
|
14
|
-
* This pattern is used by HIE systems that need to:
|
|
15
|
-
* - Verify document signing certificates before granting access
|
|
16
|
-
* - Cache CRL/OCSP responses to meet response time SLAs
|
|
17
|
-
*
|
|
18
|
-
* Prerequisites:
|
|
19
|
-
* npm install @certysign/sdk
|
|
20
|
-
* export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
|
|
21
|
-
* export CERTYSIGN_SECRET_KEY="cs_sk_..."
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
'use strict';
|
|
25
|
-
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
const path = require('path');
|
|
28
|
-
const { CertySignClient, CertySignError } = require('@certysign/sdk');
|
|
29
|
-
|
|
30
|
-
const client = new CertySignClient({
|
|
31
|
-
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
32
|
-
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
33
|
-
environment: 'production'
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
async function runCertificateFlow() {
|
|
37
|
-
console.log('\n[PKI] CertySign Certificate Lifecycle Demo');
|
|
38
|
-
console.log('─'.repeat(60));
|
|
39
|
-
|
|
40
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
41
|
-
// 1. Issue a certificate
|
|
42
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
43
|
-
console.log('\n[PKI] 1. Issuing X.509 certificate...');
|
|
44
|
-
|
|
45
|
-
const issueResult = await client.certificates.issue({
|
|
46
|
-
commonName: 'HIE Integration Node — KNH',
|
|
47
|
-
organisation: 'Kenyatta National Hospital',
|
|
48
|
-
country: 'KE',
|
|
49
|
-
state: 'Nairobi',
|
|
50
|
-
locality: 'Nairobi',
|
|
51
|
-
email: 'pki@knh.or.ke',
|
|
52
|
-
validityDays: 365,
|
|
53
|
-
metadata: { system: 'HIE-NODE-KNH-PROD', issuedAt: new Date().toISOString() }
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const cert = issueResult.data;
|
|
57
|
-
console.log(` serialNumber : ${cert.serialNumber}`);
|
|
58
|
-
console.log(` fingerprint : ${cert.fingerprint}`);
|
|
59
|
-
console.log(` validFrom : ${cert.validFrom}`);
|
|
60
|
-
console.log(` validUntil : ${cert.validUntil}`);
|
|
61
|
-
console.log(` Subject CN : ${cert.subject?.cn}`);
|
|
62
|
-
|
|
63
|
-
// Store private key securely (shown ONCE — CertySign does not retain it)
|
|
64
|
-
const keyPath = path.join(__dirname, 'hie-node-knh.key.pem');
|
|
65
|
-
fs.writeFileSync(keyPath, cert.privateKey, { mode: 0o600 });
|
|
66
|
-
console.log(` privateKey → ${keyPath} (mode 600)`);
|
|
67
|
-
|
|
68
|
-
const certPath = path.join(__dirname, 'hie-node-knh.crt.pem');
|
|
69
|
-
fs.writeFileSync(certPath, cert.pemCertificate);
|
|
70
|
-
console.log(` certificate → ${certPath}`);
|
|
71
|
-
|
|
72
|
-
const { serialNumber } = cert;
|
|
73
|
-
|
|
74
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
75
|
-
// 2. Real-time status check (should be 'good')
|
|
76
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
77
|
-
console.log('\n[PKI] 2. Certificate status check...');
|
|
78
|
-
const statusResult = await client.certificates.status(serialNumber);
|
|
79
|
-
const status = statusResult.data;
|
|
80
|
-
console.log(` status : ${status.status}`);
|
|
81
|
-
if (status.status === 'revoked') {
|
|
82
|
-
console.log(` revocationDate : ${status.revocationDate}`);
|
|
83
|
-
console.log(` reason : ${status.revocationReason}`);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
87
|
-
// 3. Full certificate verification (chain + revocation)
|
|
88
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
89
|
-
console.log('\n[PKI] 3. Full certificate verification...');
|
|
90
|
-
const verifyResult = await client.certificates.verify(serialNumber);
|
|
91
|
-
const verification = verifyResult.data;
|
|
92
|
-
console.log(` valid : ${verification.valid}`);
|
|
93
|
-
console.log(` chainVerified : ${verification.chainVerified}`);
|
|
94
|
-
console.log(` revocationChecked : ${verification.revocationChecked}`);
|
|
95
|
-
console.log(` certStatus : ${verification.certStatus}`);
|
|
96
|
-
|
|
97
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
98
|
-
// 4. Download CA chain and install in local trust store
|
|
99
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
100
|
-
console.log('\n[PKI] 4. Downloading CA chain...');
|
|
101
|
-
const chainPem = await client.pki.chain();
|
|
102
|
-
const chainPath = path.join(__dirname, 'certysign-chain.pem');
|
|
103
|
-
fs.writeFileSync(chainPath, chainPem);
|
|
104
|
-
console.log(` CA chain → ${chainPath}`);
|
|
105
|
-
console.log(` (${chainPem.split('-----BEGIN CERTIFICATE-----').length - 1} cert(s) in bundle)`);
|
|
106
|
-
|
|
107
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
108
|
-
// 5. Point-in-time verification
|
|
109
|
-
// Used by HIE to check if a cert was valid at the time a document was signed
|
|
110
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
111
|
-
console.log('\n[PKI] 5. Point-in-time verification...');
|
|
112
|
-
const signingTime = new Date(Date.now() - 5 * 60 * 1000); // 5 minutes ago
|
|
113
|
-
const ptResult = await client.certificates.verify(serialNumber, signingTime);
|
|
114
|
-
console.log(` valid at ${signingTime.toISOString()} : ${ptResult.data.valid}`);
|
|
115
|
-
|
|
116
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
117
|
-
// 6. Fetch and cache CRL (JSON format for application-level checks)
|
|
118
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
119
|
-
console.log('\n[PKI] 6. Fetching CRL (JSON)...');
|
|
120
|
-
const crlJson = await client.pki.crl('json');
|
|
121
|
-
const crl = crlJson.data;
|
|
122
|
-
console.log(` issuer : ${crl.issuer}`);
|
|
123
|
-
console.log(` thisUpdate : ${crl.thisUpdate}`);
|
|
124
|
-
console.log(` nextUpdate : ${crl.nextUpdate}`);
|
|
125
|
-
console.log(` revokedCount : ${crl.revokedCount}`);
|
|
126
|
-
|
|
127
|
-
// Build a Set of revoked serial numbers for fast in-process lookups
|
|
128
|
-
const revokedSet = new Set((crl.crl || []).map(e => e.serialNumber));
|
|
129
|
-
console.log(` Revoked set : ${revokedSet.size} entries loaded`);
|
|
130
|
-
|
|
131
|
-
// Cache on disk until nextUpdate
|
|
132
|
-
fs.writeFileSync(
|
|
133
|
-
path.join(__dirname, 'crl-cache.json'),
|
|
134
|
-
JSON.stringify({ ...crl, cachedAt: new Date().toISOString() }, null, 2)
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
// Also download DER CRL for systems that need binary format
|
|
138
|
-
console.log('\n[PKI] 6b. Downloading DER CRL...');
|
|
139
|
-
const crlDer = await client.pki.crl('der');
|
|
140
|
-
fs.writeFileSync(path.join(__dirname, 'certysign-intermediate.crl'), crlDer);
|
|
141
|
-
console.log(` certysign-intermediate.crl — ${crlDer.length} bytes`);
|
|
142
|
-
|
|
143
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
144
|
-
// 7. In-process revocation check using cached CRL
|
|
145
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
146
|
-
console.log('\n[PKI] 7. Local CRL check...');
|
|
147
|
-
const isRevoked = revokedSet.has(serialNumber);
|
|
148
|
-
console.log(` Serial ${serialNumber.slice(0, 12)}... : ${isRevoked ? 'REVOKED' : 'not in CRL'}`);
|
|
149
|
-
|
|
150
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
151
|
-
// 8. OCSP query — freshest revocation status
|
|
152
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
153
|
-
console.log('\n[PKI] 8. OCSP query...');
|
|
154
|
-
const ocspResult = await client.pki.ocsp(serialNumber, 'json');
|
|
155
|
-
const ocsp = ocspResult.data;
|
|
156
|
-
console.log(` status : ${ocsp.status}`);
|
|
157
|
-
console.log(` producedAt : ${ocsp.producedAt}`);
|
|
158
|
-
console.log(` responderId : ${ocsp.responderId}`);
|
|
159
|
-
console.log(` nextUpdate : ${ocsp.nextUpdate}`);
|
|
160
|
-
console.log(` signatureAlgo : ${ocsp.signatureAlgorithm}`);
|
|
161
|
-
if (ocsp.certIdHash) {
|
|
162
|
-
console.log(` CertID algo : ${ocsp.certIdHash.algorithm}`);
|
|
163
|
-
console.log(` issuerNameHash : ${ocsp.certIdHash.issuerNameHash?.slice(0, 16)}...`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
167
|
-
// 9. CA Info — inspect the trust hierarchy
|
|
168
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
169
|
-
console.log('\n[PKI] 9. CA hierarchy info...');
|
|
170
|
-
const info = await client.pki.info();
|
|
171
|
-
const caInfo = info.data;
|
|
172
|
-
console.log(` Root CA : ${caInfo.rootCA?.subject?.commonName}`);
|
|
173
|
-
console.log(` Root CA expiry : ${caInfo.rootCA?.validity?.notAfter}`);
|
|
174
|
-
console.log(` Inter CA : ${caInfo.intermediateCA?.subject?.commonName}`);
|
|
175
|
-
console.log(` Inter CA expiry: ${caInfo.intermediateCA?.validity?.notAfter}`);
|
|
176
|
-
console.log(` CRL URL : ${caInfo.intermediateCA?.crlUrl}`);
|
|
177
|
-
console.log(` OCSP URL : ${caInfo.intermediateCA?.ocspUrl}`);
|
|
178
|
-
console.log(` Initialised : ${caInfo.status?.initialized}`);
|
|
179
|
-
|
|
180
|
-
console.log('\n[PKI] ✓ Certificate lifecycle demo complete');
|
|
181
|
-
console.log('─'.repeat(60));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
185
|
-
|
|
186
|
-
(async () => {
|
|
187
|
-
try {
|
|
188
|
-
await runCertificateFlow();
|
|
189
|
-
} catch (err) {
|
|
190
|
-
if (err instanceof CertySignError) {
|
|
191
|
-
console.error(`\n[PKI] API Error (${err.statusCode}): ${err.message}`);
|
|
192
|
-
if (err.details) console.error('[PKI] Details:', JSON.stringify(err.details, null, 2));
|
|
193
|
-
} else {
|
|
194
|
-
console.error('\n[PKI] Error:', err.message);
|
|
195
|
-
}
|
|
196
|
-
process.exit(1);
|
|
197
|
-
}
|
|
198
|
-
})();
|
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DHA Kenya — Health Claims Quick-Sign Integration
|
|
3
|
-
*
|
|
4
|
-
* This example demonstrates the standard integration pattern for the
|
|
5
|
-
* Department of Health Affairs (DHA) Kenya claims processing system.
|
|
6
|
-
*
|
|
7
|
-
* Workflow:
|
|
8
|
-
* 1. Load a health claim PDF
|
|
9
|
-
* 2. Sign it using quickSign() — creates envelope, uploads, signs in one call
|
|
10
|
-
* 3. Issue a per-document X.509 certificate bound to the claim
|
|
11
|
-
* 4. Store the envelopeId and certificate serial number in the claims DB
|
|
12
|
-
* 5. Optionally verify the certificate via OCSP before archiving
|
|
13
|
-
*
|
|
14
|
-
* Prerequisites:
|
|
15
|
-
* npm install @certysign/sdk
|
|
16
|
-
* export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
|
|
17
|
-
* export CERTYSIGN_SECRET_KEY="cs_sk_..."
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
'use strict';
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
const { CertySignClient } = require('@certysign/sdk');
|
|
25
|
-
|
|
26
|
-
// ── Initialise client ────────────────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const client = new CertySignClient({
|
|
29
|
-
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
30
|
-
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
31
|
-
environment: 'production' // 'staging' for UAT
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// ── Simulated DHA claim record ───────────────────────────────────────────────
|
|
35
|
-
|
|
36
|
-
const claim = {
|
|
37
|
-
id: 'CLM-2026-003421',
|
|
38
|
-
patientName: 'John Kamau Njoroge',
|
|
39
|
-
facility: 'Kenyatta National Hospital',
|
|
40
|
-
approvedBy: 'Dr. Amina Okonkwo',
|
|
41
|
-
approverEmail: 'amina.okonkwo@dha.go.ke',
|
|
42
|
-
department: 'Primary Health Outreach',
|
|
43
|
-
amount: 45_000 // KES
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
// ── Main integration function ────────────────────────────────────────────────
|
|
47
|
-
|
|
48
|
-
async function processClaim(claim) {
|
|
49
|
-
console.log(`\n[DHA] Processing claim ${claim.id} — ${claim.facility}`);
|
|
50
|
-
console.log('─'.repeat(60));
|
|
51
|
-
|
|
52
|
-
// Step 1: Load the generated claim PDF (your system produces this)
|
|
53
|
-
const pdfPath = path.join(__dirname, 'sample-claim.pdf');
|
|
54
|
-
let pdfBuffer;
|
|
55
|
-
try {
|
|
56
|
-
pdfBuffer = fs.readFileSync(pdfPath);
|
|
57
|
-
} catch {
|
|
58
|
-
// Create a minimal placeholder for demo purposes
|
|
59
|
-
pdfBuffer = Buffer.from('%PDF-1.4 sample claim document');
|
|
60
|
-
console.warn('[DHA] Using placeholder PDF — replace with real claim PDF in production');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
64
|
-
// Step 2: Sign the claim document
|
|
65
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
66
|
-
console.log('\n[DHA] Signing claim document...');
|
|
67
|
-
|
|
68
|
-
const signResult = await client.sign.quickSign({
|
|
69
|
-
document: pdfBuffer,
|
|
70
|
-
filename: `claim-${claim.id}.pdf`,
|
|
71
|
-
signerName: claim.approvedBy,
|
|
72
|
-
signerEmail: claim.approverEmail,
|
|
73
|
-
reason: `Health claim approval — DHA Kenya (${claim.id})`,
|
|
74
|
-
location: 'Nairobi, Kenya',
|
|
75
|
-
metadata: {
|
|
76
|
-
claimId: claim.id,
|
|
77
|
-
patientName: claim.patientName,
|
|
78
|
-
facility: claim.facility,
|
|
79
|
-
department: claim.department,
|
|
80
|
-
amount: claim.amount,
|
|
81
|
-
currency: 'KES',
|
|
82
|
-
system: 'DHA-CLAIMS-PROD'
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const { envelopeId, certificate: sigCert } = signResult.data;
|
|
87
|
-
console.log(`[DHA] ✓ Document signed`);
|
|
88
|
-
console.log(` envelopeId : ${envelopeId}`);
|
|
89
|
-
console.log(` sigCert SN : ${sigCert?.serialNumber || '(embedded in PDF)'}`);
|
|
90
|
-
|
|
91
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
92
|
-
// Step 3: Issue a per-document X.509 certificate for the claim
|
|
93
|
-
// This cert is used by the HIE system to grant document access
|
|
94
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
95
|
-
console.log('\n[DHA] Issuing per-document X.509 certificate...');
|
|
96
|
-
|
|
97
|
-
const certResult = await client.certificates.issue({
|
|
98
|
-
commonName: `DHA-${claim.id}`,
|
|
99
|
-
organisation: 'Department of Health Affairs Kenya',
|
|
100
|
-
country: 'KE',
|
|
101
|
-
state: 'Nairobi',
|
|
102
|
-
locality: 'Nairobi',
|
|
103
|
-
email: claim.approverEmail,
|
|
104
|
-
validityDays: 365,
|
|
105
|
-
metadata: {
|
|
106
|
-
claimId: claim.id,
|
|
107
|
-
envelopeId,
|
|
108
|
-
facility: claim.facility,
|
|
109
|
-
issuedAt: new Date().toISOString()
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
const cert = certResult.data;
|
|
114
|
-
console.log(`[DHA] ✓ Certificate issued`);
|
|
115
|
-
console.log(` serialNumber : ${cert.serialNumber}`);
|
|
116
|
-
console.log(` fingerprint : ${cert.fingerprint}`);
|
|
117
|
-
console.log(` validUntil : ${cert.validUntil}`);
|
|
118
|
-
console.log(` privateKey : [SHOW ONCE — store securely]`);
|
|
119
|
-
|
|
120
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
121
|
-
// Step 4: Verify the certificate is good via OCSP
|
|
122
|
-
// (optional but recommended before archiving)
|
|
123
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
-
console.log('\n[DHA] Verifying certificate via OCSP...');
|
|
125
|
-
|
|
126
|
-
const ocspResult = await client.pki.ocsp(cert.serialNumber, 'json');
|
|
127
|
-
const ocspStatus = ocspResult.data;
|
|
128
|
-
|
|
129
|
-
console.log(`[DHA] OCSP status : ${ocspStatus.status}`);
|
|
130
|
-
console.log(` responderId : ${ocspStatus.responderId}`);
|
|
131
|
-
console.log(` nextUpdate : ${ocspStatus.nextUpdate}`);
|
|
132
|
-
|
|
133
|
-
if (ocspStatus.status !== 'good') {
|
|
134
|
-
throw new Error(`[DHA] Unexpected OCSP status: ${ocspStatus.status}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
138
|
-
// Step 5: Store claim record with signing proof
|
|
139
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
140
|
-
const claimRecord = {
|
|
141
|
-
claimId: claim.id,
|
|
142
|
-
envelopeId,
|
|
143
|
-
certificateSerial: cert.serialNumber,
|
|
144
|
-
certificateExpiry: cert.validUntil,
|
|
145
|
-
signedAt: new Date().toISOString(),
|
|
146
|
-
status: 'SIGNED',
|
|
147
|
-
// Store PEM cert for offline verification (NOT the private key in production DB)
|
|
148
|
-
pemCertificate: cert.pemCertificate
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
console.log('\n[DHA] ✓ Claim processed successfully');
|
|
152
|
-
console.log('─'.repeat(60));
|
|
153
|
-
console.log('[DHA] Claim record:', JSON.stringify(claimRecord, null, 2));
|
|
154
|
-
|
|
155
|
-
return claimRecord;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ── Retrieve and verify an archived claim ────────────────────────────────────
|
|
159
|
-
|
|
160
|
-
async function verifyClaim(claimRecord) {
|
|
161
|
-
console.log(`\n[DHA] Verifying archived claim ${claimRecord.claimId}`);
|
|
162
|
-
console.log('─'.repeat(60));
|
|
163
|
-
|
|
164
|
-
// Check signing envelope still has valid signatures
|
|
165
|
-
const envelopeResult = await client.sign.verifyById(claimRecord.envelopeId);
|
|
166
|
-
const env = envelopeResult.data;
|
|
167
|
-
|
|
168
|
-
console.log(`[DHA] Envelope status : ${env.status}`);
|
|
169
|
-
console.log(`[DHA] Signature valid : ${env.valid}`);
|
|
170
|
-
console.log(`[DHA] Signed by : ${env.signerName}`);
|
|
171
|
-
console.log(`[DHA] Signed at : ${env.signedAt}`);
|
|
172
|
-
|
|
173
|
-
// Check certificate status
|
|
174
|
-
const certStatus = await client.certificates.status(claimRecord.certificateSerial);
|
|
175
|
-
console.log(`[DHA] Certificate status: ${certStatus.data.status}`);
|
|
176
|
-
|
|
177
|
-
// Point-in-time verification — was cert valid when signing happened?
|
|
178
|
-
const certAtTime = await client.certificates.verify(
|
|
179
|
-
claimRecord.certificateSerial,
|
|
180
|
-
claimRecord.signedAt
|
|
181
|
-
);
|
|
182
|
-
console.log(`[DHA] Cert valid-at-signing: ${certAtTime.data.valid}`);
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
envelopeValid: env.valid,
|
|
186
|
-
certificateStatus: certStatus.data.status,
|
|
187
|
-
certValidAtSigning: certAtTime.data.valid
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ── Run example ──────────────────────────────────────────────────────────────
|
|
192
|
-
|
|
193
|
-
(async () => {
|
|
194
|
-
try {
|
|
195
|
-
const record = await processClaim(claim);
|
|
196
|
-
await verifyClaim(record);
|
|
197
|
-
} catch (err) {
|
|
198
|
-
if (err.code === 'INVALID_API_KEY') {
|
|
199
|
-
console.error('\n[DHA] Authentication failed — check CERTYSIGN_PUBLIC_KEY / CERTYSIGN_SECRET_KEY');
|
|
200
|
-
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
|
|
201
|
-
console.error('\n[DHA] Rate limit reached — implement request throttling');
|
|
202
|
-
} else {
|
|
203
|
-
console.error('\n[DHA] Error:', err.message);
|
|
204
|
-
if (err.details) console.error('[DHA] Details:', JSON.stringify(err.details, null, 2));
|
|
205
|
-
}
|
|
206
|
-
process.exit(1);
|
|
207
|
-
}
|
|
208
|
-
})();
|