@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,208 @@
|
|
|
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
|
+
})();
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NHIF Kenya — Batch Invoice Signing Integration
|
|
3
|
+
*
|
|
4
|
+
* This example shows how NHIF's automated invoicing system uses batchSign()
|
|
5
|
+
* to sign hundreds of provider reimbursement invoices in a single API call
|
|
6
|
+
* during the nightly batch run.
|
|
7
|
+
*
|
|
8
|
+
* Workflow:
|
|
9
|
+
* 1. Collect pending invoices from the DB (up to 25 per API call)
|
|
10
|
+
* 2. Sign all documents in one batchSign() call
|
|
11
|
+
* 3. For each signed invoice: store envelopeId, certificate serial
|
|
12
|
+
* 4. Retrieve audit trails for compliance archiving
|
|
13
|
+
* 5. Download signed PDFs from the envelopes
|
|
14
|
+
*
|
|
15
|
+
* Rate limits:
|
|
16
|
+
* The SDK automatically handles 429 responses with exponential back-off.
|
|
17
|
+
* For very large batches, use the BATCH_SIZE constant to chunk requests.
|
|
18
|
+
*
|
|
19
|
+
* Prerequisites:
|
|
20
|
+
* npm install @certysign/sdk
|
|
21
|
+
* export CERTYSIGN_PUBLIC_KEY="cs_pk_..."
|
|
22
|
+
* export CERTYSIGN_SECRET_KEY="cs_sk_..."
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
'use strict';
|
|
26
|
+
|
|
27
|
+
const fs = require('fs');
|
|
28
|
+
const path = require('path');
|
|
29
|
+
const { CertySignClient, CertySignError } = require('@certysign/sdk');
|
|
30
|
+
|
|
31
|
+
// ── Config ───────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const BATCH_SIZE = 10; // Sign up to 10 invoices per API call (max 25)
|
|
34
|
+
const SIGNER_NAME = 'NHIF Finance System';
|
|
35
|
+
const SIGNER_EMAIL = 'invoicing-batch@nhif.or.ke';
|
|
36
|
+
|
|
37
|
+
const client = new CertySignClient({
|
|
38
|
+
publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
39
|
+
secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
40
|
+
environment: 'production'
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Simulated pending invoices ────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
function getPendingInvoices() {
|
|
46
|
+
// In production: fetch from your DB
|
|
47
|
+
return [
|
|
48
|
+
{ id: 'INV-2026-0441', provider: 'Aga Khan University Hospital', amount: 1_250_000 },
|
|
49
|
+
{ id: 'INV-2026-0442', provider: 'Nairobi Hospital', amount: 875_000 },
|
|
50
|
+
{ id: 'INV-2026-0443', provider: 'Karen Hospital', amount: 430_000 },
|
|
51
|
+
{ id: 'INV-2026-0444', provider: 'MP Shah Hospital', amount: 995_000 },
|
|
52
|
+
{ id: 'INV-2026-0445', provider: 'Mater Hospital', amount: 660_000 }
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadInvoicePdf(invoice) {
|
|
57
|
+
// In production: load from document store / generate from template
|
|
58
|
+
const filePath = path.join(__dirname, `invoice-${invoice.id}.pdf`);
|
|
59
|
+
if (fs.existsSync(filePath)) return fs.readFileSync(filePath);
|
|
60
|
+
// Placeholder for demo
|
|
61
|
+
return Buffer.from(`%PDF-1.4 NHIF Invoice ${invoice.id} — ${invoice.provider}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Chunk array helper ────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
function* chunks(arr, size) {
|
|
67
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
68
|
+
yield arr.slice(i, i + size);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Batch signing ─────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
async function runNightlyBatch() {
|
|
75
|
+
console.log('\n[NHIF] Starting nightly invoice signing batch');
|
|
76
|
+
console.log(`[NHIF] Timestamp: ${new Date().toISOString()}`);
|
|
77
|
+
console.log('─'.repeat(60));
|
|
78
|
+
|
|
79
|
+
const invoices = getPendingInvoices();
|
|
80
|
+
console.log(`[NHIF] ${invoices.length} invoices pending signature`);
|
|
81
|
+
|
|
82
|
+
const signedRecords = [];
|
|
83
|
+
const failedInvoices = [];
|
|
84
|
+
let batchNumber = 0;
|
|
85
|
+
|
|
86
|
+
for (const batch of chunks(invoices, BATCH_SIZE)) {
|
|
87
|
+
batchNumber++;
|
|
88
|
+
console.log(`\n[NHIF] Batch ${batchNumber} — signing ${batch.length} invoices...`);
|
|
89
|
+
|
|
90
|
+
// Build document list for this batch
|
|
91
|
+
const documents = batch.map(invoice => ({
|
|
92
|
+
data: loadInvoicePdf(invoice),
|
|
93
|
+
filename: `nhif-invoice-${invoice.id}.pdf`
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const result = await client.sign.batchSign({
|
|
98
|
+
documents,
|
|
99
|
+
signerName: SIGNER_NAME,
|
|
100
|
+
signerEmail: SIGNER_EMAIL,
|
|
101
|
+
reason: `NHIF provider reimbursement batch — ${new Date().toISOString().slice(0, 10)}`,
|
|
102
|
+
location: 'Nairobi, Kenya',
|
|
103
|
+
metadata: {
|
|
104
|
+
batchNumber: batchNumber,
|
|
105
|
+
invoiceCount: batch.length,
|
|
106
|
+
system: 'NHIF-FINANCE-BATCH'
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Map results back to invoice records
|
|
111
|
+
const results = result.data.results; // Array, one per document
|
|
112
|
+
for (let i = 0; i < batch.length; i++) {
|
|
113
|
+
const invoice = batch[i];
|
|
114
|
+
const res = results[i];
|
|
115
|
+
|
|
116
|
+
if (res.success) {
|
|
117
|
+
signedRecords.push({
|
|
118
|
+
invoiceId: invoice.id,
|
|
119
|
+
provider: invoice.provider,
|
|
120
|
+
amount: invoice.amount,
|
|
121
|
+
envelopeId: res.envelopeId,
|
|
122
|
+
certificateSerial: res.certificate?.serialNumber,
|
|
123
|
+
signedAt: new Date().toISOString(),
|
|
124
|
+
status: 'SIGNED'
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
console.log(` ✓ ${invoice.id} → env: ${res.envelopeId}`);
|
|
128
|
+
} else {
|
|
129
|
+
failedInvoices.push({ invoice, reason: res.error });
|
|
130
|
+
console.warn(` ✗ ${invoice.id} → ${res.error}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof CertySignError) {
|
|
136
|
+
console.error(`[NHIF] Batch ${batchNumber} API error (${err.statusCode}): ${err.message}`);
|
|
137
|
+
if (err.statusCode === 429) {
|
|
138
|
+
// Rate limited — wait and log (SDK already retried 3 times)
|
|
139
|
+
console.error('[NHIF] Rate limit exhausted — will retry this batch in next run');
|
|
140
|
+
}
|
|
141
|
+
batch.forEach(inv => failedInvoices.push({ invoice: inv, reason: err.message }));
|
|
142
|
+
} else {
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
149
|
+
// Step 2: Retrieve audit trails for compliance
|
|
150
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
151
|
+
console.log(`\n[NHIF] Retrieving audit trails for ${signedRecords.length} signed invoices...`);
|
|
152
|
+
|
|
153
|
+
for (const record of signedRecords.slice(0, 3)) { // Demo: first 3 only
|
|
154
|
+
try {
|
|
155
|
+
const auditResult = await client.envelopes.getAuditTrail(record.envelopeId);
|
|
156
|
+
const { auditTrail, chainIntegrity } = auditResult.data;
|
|
157
|
+
|
|
158
|
+
record.auditEntries = auditTrail.length;
|
|
159
|
+
record.auditChainValid = chainIntegrity?.valid;
|
|
160
|
+
|
|
161
|
+
console.log(` ✓ ${record.invoiceId}: ${auditTrail.length} events, chain=${chainIntegrity?.valid}`);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.warn(` ✗ Audit trail for ${record.invoiceId}: ${err.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
168
|
+
// Step 3: Download first signed PDF as a demo
|
|
169
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
170
|
+
if (signedRecords.length > 0) {
|
|
171
|
+
const sample = signedRecords[0];
|
|
172
|
+
try {
|
|
173
|
+
console.log(`\n[NHIF] Downloading signed PDF for ${sample.invoiceId}...`);
|
|
174
|
+
// First get the envelope to find the document ID
|
|
175
|
+
const env = await client.envelopes.get(sample.envelopeId);
|
|
176
|
+
const docId = env.data?.envelope?.documents?.[0]?._id;
|
|
177
|
+
|
|
178
|
+
if (docId) {
|
|
179
|
+
const pdfBuffer = await client.envelopes.getDocument(sample.envelopeId, docId);
|
|
180
|
+
const outPath = path.join(__dirname, `signed-${sample.invoiceId}.pdf`);
|
|
181
|
+
fs.writeFileSync(outPath, pdfBuffer);
|
|
182
|
+
console.log(`[NHIF] ✓ Saved signed PDF to ${outPath} (${pdfBuffer.length} bytes)`);
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.warn(`[NHIF] Could not download signed PDF: ${err.message}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
190
|
+
// Summary
|
|
191
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
192
|
+
console.log('\n' + '─'.repeat(60));
|
|
193
|
+
console.log(`[NHIF] Batch complete`);
|
|
194
|
+
console.log(` Signed : ${signedRecords.length}`);
|
|
195
|
+
console.log(` Failed : ${failedInvoices.length}`);
|
|
196
|
+
|
|
197
|
+
if (failedInvoices.length) {
|
|
198
|
+
console.log('[NHIF] Failed invoices:');
|
|
199
|
+
failedInvoices.forEach(({ invoice, reason }) => {
|
|
200
|
+
console.log(` - ${invoice.id}: ${reason}`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { signed: signedRecords, failed: failedInvoices };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
(async () => {
|
|
210
|
+
try {
|
|
211
|
+
await runNightlyBatch();
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error('\n[NHIF] Fatal error:', err.message);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
})();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@certysign/sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Official Node.js SDK for CertySign — digital signing, X.509 certificates, and PKI services",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"types": "src/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "jest --coverage",
|
|
9
|
+
"lint": "eslint src/**/*.js",
|
|
10
|
+
"build:types": "npx tsc --emitDeclarationOnly"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"certysign",
|
|
14
|
+
"digital-signature",
|
|
15
|
+
"pki",
|
|
16
|
+
"x509",
|
|
17
|
+
"pdf-signing",
|
|
18
|
+
"document-signing",
|
|
19
|
+
"kenya",
|
|
20
|
+
"pades"
|
|
21
|
+
],
|
|
22
|
+
"author": "CertySign Limited <sdk@certysign.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"axios": "^1.6.0",
|
|
29
|
+
"form-data": "^4.0.0"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"jest": "^29.7.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/certysign/sdk-node.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://docs.certysign.com/sdk",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/certysign/sdk-node/issues"
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview CertySign SDK for Node.js
|
|
3
|
+
*
|
|
4
|
+
* Official client library for CertySign Trust Services — digital document
|
|
5
|
+
* signing, X.509 certificate issuance, and PKI operations for East Africa.
|
|
6
|
+
*
|
|
7
|
+
* @example Basic setup
|
|
8
|
+
* ```js
|
|
9
|
+
* const { CertySignClient } = require('@certysign/sdk');
|
|
10
|
+
*
|
|
11
|
+
* const client = new CertySignClient({
|
|
12
|
+
* publicKey: 'cs_pk_...',
|
|
13
|
+
* secretKey: 'cs_sk_...'
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { HttpClient, CertySignError } = require('./lib/HttpClient');
|
|
21
|
+
const { SigningResource } = require('./lib/SigningResource');
|
|
22
|
+
const { CertificateResource } = require('./lib/CertificateResource');
|
|
23
|
+
const { PkiResource } = require('./lib/PkiResource');
|
|
24
|
+
const { EnvelopeResource } = require('./lib/EnvelopeResource');
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* CertySign API client.
|
|
28
|
+
*
|
|
29
|
+
* Resources are accessed as properties:
|
|
30
|
+
* - client.sign — document signing (quickSign, batchSign, verify)
|
|
31
|
+
* - client.certificates — X.509 certificate lifecycle (issue, verify, status)
|
|
32
|
+
* - client.pki — PKI infrastructure (CRL, OCSP, CA chain, info)
|
|
33
|
+
* - client.envelopes — envelope management (create, upload, send, sign, audit)
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const { CertySignClient } = require('@certysign/sdk');
|
|
37
|
+
*
|
|
38
|
+
* const client = new CertySignClient({
|
|
39
|
+
* publicKey: process.env.CERTYSIGN_PUBLIC_KEY,
|
|
40
|
+
* secretKey: process.env.CERTYSIGN_SECRET_KEY,
|
|
41
|
+
* environment: 'production' // or 'staging', 'development'
|
|
42
|
+
* });
|
|
43
|
+
*
|
|
44
|
+
* // Sign a document
|
|
45
|
+
* const result = await client.sign.quickSign({
|
|
46
|
+
* document: fs.readFileSync('./claim.pdf'),
|
|
47
|
+
* filename: 'claim.pdf',
|
|
48
|
+
* signerName: 'Dr. Amina Okonkwo',
|
|
49
|
+
* reason: 'Health records approval'
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* console.log(result.data.envelopeId);
|
|
53
|
+
* console.log(result.data.certificate.serialNumber);
|
|
54
|
+
*/
|
|
55
|
+
class CertySignClient {
|
|
56
|
+
/**
|
|
57
|
+
* @param {ClientOptions} options
|
|
58
|
+
*/
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
const {
|
|
61
|
+
publicKey,
|
|
62
|
+
secretKey,
|
|
63
|
+
baseUrl,
|
|
64
|
+
environment = 'production',
|
|
65
|
+
timeout,
|
|
66
|
+
retries,
|
|
67
|
+
debug = false
|
|
68
|
+
} = options;
|
|
69
|
+
|
|
70
|
+
if (!publicKey) throw new Error('CertySignClient: publicKey is required');
|
|
71
|
+
if (!secretKey) throw new Error('CertySignClient: secretKey is required');
|
|
72
|
+
|
|
73
|
+
// Resolve base URL from environment if not explicitly provided
|
|
74
|
+
const resolvedBaseUrl = baseUrl ?? CertySignClient.BASE_URLS[environment];
|
|
75
|
+
if (!resolvedBaseUrl) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`CertySignClient: unknown environment "${environment}". ` +
|
|
78
|
+
`Expected one of: ${Object.keys(CertySignClient.BASE_URLS).join(', ')} ` +
|
|
79
|
+
`or provide baseUrl directly.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
this._http = new HttpClient({
|
|
84
|
+
publicKey,
|
|
85
|
+
secretKey,
|
|
86
|
+
baseUrl: resolvedBaseUrl,
|
|
87
|
+
timeout,
|
|
88
|
+
retries,
|
|
89
|
+
debug
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Resource objects ──
|
|
93
|
+
this.sign = new SigningResource(this._http);
|
|
94
|
+
this.certificates = new CertificateResource(this._http);
|
|
95
|
+
this.pki = new PkiResource(this._http);
|
|
96
|
+
this.envelopes = new EnvelopeResource(this._http);
|
|
97
|
+
|
|
98
|
+
this.publicKey = publicKey;
|
|
99
|
+
this.environment = environment;
|
|
100
|
+
this.baseUrl = resolvedBaseUrl;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Environment → base URL mapping.
|
|
105
|
+
* Override any entry via the `baseUrl` constructor option.
|
|
106
|
+
*/
|
|
107
|
+
static get BASE_URLS() {
|
|
108
|
+
return {
|
|
109
|
+
production: 'https://api.certysign.com',
|
|
110
|
+
staging: 'https://api-staging.certysign.com',
|
|
111
|
+
development: 'http://localhost:8000',
|
|
112
|
+
test: 'http://localhost:8000'
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Verify that the API key is valid and return the key metadata.
|
|
118
|
+
* Useful as a "ping" / health check at startup.
|
|
119
|
+
*
|
|
120
|
+
* @returns {Promise<{ success: boolean, data: { keyName, permissions, environment, tenantId } }>}
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* const { data } = await client.ping();
|
|
124
|
+
* console.log('Connected as:', data.keyName);
|
|
125
|
+
* console.log('Permissions:', data.permissions);
|
|
126
|
+
*/
|
|
127
|
+
async ping() {
|
|
128
|
+
// Issue a lightweight request; GET /pki/info is cheap and requires pki:info
|
|
129
|
+
try {
|
|
130
|
+
const result = await this._http.get('/sdk/v1/pki/info');
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
data: {
|
|
134
|
+
keyName: result.data?.keyName,
|
|
135
|
+
permissions: result.data?.permissions,
|
|
136
|
+
environment: result.data?.environment ?? this.environment,
|
|
137
|
+
tenantId: result.data?.tenantId,
|
|
138
|
+
caInitialized: result.data?.status?.initialized
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
} catch (err) {
|
|
142
|
+
return { success: false, error: err.message, code: err.code };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* @typedef {Object} ClientOptions
|
|
149
|
+
* @property {string} publicKey - API public key (cs_pk_...)
|
|
150
|
+
* @property {string} secretKey - API secret key (cs_sk_...)
|
|
151
|
+
* @property {'production'|'staging'|'development'|'test'} [environment] - Target environment (default: 'production')
|
|
152
|
+
* @property {string} [baseUrl] - Override the API base URL (e.g. for self-hosted)
|
|
153
|
+
* @property {number} [timeout] - Request timeout in ms (default: 30000)
|
|
154
|
+
* @property {number} [retries] - Max retries on transient errors (default: 3)
|
|
155
|
+
* @property {boolean} [debug] - Log HTTP requests/responses (default: false)
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
module.exports = {
|
|
159
|
+
CertySignClient,
|
|
160
|
+
CertySignError
|
|
161
|
+
};
|